OSX-For-Omarchy/osx-kvm-installer.sh
28allday a5c460c328 Initial commit: macOS VM installer for Omarchy
Interactive installer for running macOS on QEMU/KVM with OpenCore.
Supports High Sierra through Tahoe, multi-VM management, Samba shares,
USB passthrough, SMBIOS generation, and a gum-based TUI manager.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:19:20 +00:00

2179 lines
71 KiB
Bash
Executable file

#!/bin/bash
# OSX-KVM Installer for Omarchy (Arch Linux)
# This script sets up everything needed to run macOS in a VM using QEMU/KVM
set -eo pipefail
# Cleanup function for failures
cleanup() {
local exit_code=$?
# Don't print error on user interrupt (Ctrl+C = 130) or clean exit
if [[ $exit_code -ne 0 && $exit_code -ne 130 ]]; then
print_error "Installation failed. Check the error messages above."
fi
}
trap cleanup EXIT
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Installation directory
INSTALL_DIR="$HOME/OSX-KVM"
VMS_DIR="$INSTALL_DIR/vms"
# Current VM being configured (set during VM creation)
CURRENT_VM_NAME=""
CURRENT_VM_DIR=""
CURRENT_MACOS_VERSION=""
CURRENT_MACOS_DISPLAY_NAME=""
CURRENT_VM_RAM=""
CURRENT_VM_CORES=""
CURRENT_VM_THREADS=""
# Feature configuration (set during VM setup)
USB_PASSTHROUGH_ARGS=""
USB_PASSTHROUGH_DEVICES=()
SMBIOS_GENERATED=false
SMBIOS_MODEL=""
SMBIOS_SERIAL=""
SMBIOS_MLB=""
SMBIOS_UUID=""
BOOT_ARGS=""
print_banner() {
echo -e "${BLUE}"
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ OSX-KVM Installer for Omarchy ║"
echo "║ Run macOS on QEMU/KVM with OpenCore ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
print_status() {
echo -e "${GREEN}[✓]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[!]${NC} $1"
}
print_error() {
echo -e "${RED}[✗]${NC} $1"
}
print_info() {
echo -e "${BLUE}[i]${NC} $1"
}
# Map version arg to display name
get_macos_display_name() {
case $1 in
high-sierra) echo "macOS High Sierra" ;;
mojave) echo "macOS Mojave" ;;
catalina) echo "macOS Catalina" ;;
big-sur) echo "macOS Big Sur" ;;
monterey) echo "macOS Monterey" ;;
ventura) echo "macOS Ventura" ;;
sonoma) echo "macOS Sonoma" ;;
sequoia) echo "macOS Sequoia" ;;
tahoe) echo "macOS Tahoe" ;;
*) echo "macOS" ;;
esac
}
# List existing VMs
list_existing_vms() {
local vms=()
if [[ -d "$VMS_DIR" ]]; then
shopt -s nullglob
for vm_dir in "$VMS_DIR"/*/; do
if [[ -d "$vm_dir" ]]; then
local vm_name
vm_name=$(basename "$vm_dir")
local version_file="$vm_dir/.macos-version"
local display_name="Unknown"
if [[ -f "$version_file" ]]; then
display_name=$(tr -d '\n' < "$version_file")
fi
vms+=("$vm_name:$display_name")
fi
done
shopt -u nullglob
fi
echo "${vms[@]}"
}
# Count existing VMs
count_existing_vms() {
local count=0
if [[ -d "$VMS_DIR" ]]; then
shopt -s nullglob
for vm_dir in "$VMS_DIR"/*/; do
if [[ -d "$vm_dir" && -f "$vm_dir/mac_hdd_ng.img" ]]; then
((count++)) || true
fi
done
shopt -u nullglob
fi
echo "$count"
}
# Check for legacy installation (old single-VM style)
check_legacy_installation() {
if [[ -f "$INSTALL_DIR/mac_hdd_ng.img" ]] && [[ ! -d "$VMS_DIR" ]]; then
return 0 # Legacy installation found
fi
return 1
}
# Migrate legacy installation to new multi-VM structure
migrate_legacy_installation() {
print_info "Found legacy single-VM installation"
echo ""
echo "Your existing macOS VM will be migrated to the new multi-VM structure."
echo "This allows you to have multiple VMs with different macOS versions."
echo ""
read -rp "Enter a name for your existing VM [legacy]: " legacy_name
legacy_name=${legacy_name:-legacy}
legacy_name=$(echo "$legacy_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-_')
# Create VMs directory and VM subdirectory
mkdir -p "$VMS_DIR/$legacy_name"
# Move disk image
if [[ -f "$INSTALL_DIR/mac_hdd_ng.img" ]]; then
mv "$INSTALL_DIR/mac_hdd_ng.img" "$VMS_DIR/$legacy_name/"
print_status "Moved disk image to $VMS_DIR/$legacy_name/"
fi
# Move or copy BaseSystem.img if it exists
if [[ -f "$INSTALL_DIR/BaseSystem.img" ]]; then
mv "$INSTALL_DIR/BaseSystem.img" "$VMS_DIR/$legacy_name/"
print_status "Moved BaseSystem.img to $VMS_DIR/$legacy_name/"
fi
# Create version file (unknown version for legacy)
echo "macOS (Migrated)" > "$VMS_DIR/$legacy_name/.macos-version"
# Set current VM variables for launcher creation
CURRENT_VM_NAME="$legacy_name"
CURRENT_VM_DIR="$VMS_DIR/$legacy_name"
CURRENT_MACOS_DISPLAY_NAME="macOS (Migrated)"
# Set default resources for migrated VM (can be changed later in start-macos.sh)
CURRENT_VM_RAM=8192 # 8GB
CURRENT_VM_CORES=4
CURRENT_VM_THREADS=8
# Save default config
cat > "$CURRENT_VM_DIR/.vm-config" << EOF
RAM_GB=8
RAM_MIB=8192
CPU_CORES=4
CPU_THREADS=8
EOF
# Create new launcher for migrated VM
create_launcher
create_desktop_entry
# Remove old launcher and desktop entry if they exist
rm -f "$INSTALL_DIR/start-macos.sh"
rm -f "$HOME/.local/share/applications/macos-vm.desktop"
print_status "Migration complete!"
echo ""
}
# Show existing VMs menu
show_vm_menu() {
echo ""
echo -e "${BLUE}Existing macOS VMs:${NC}"
echo ""
local i=1
local vm_list=()
shopt -s nullglob
for vm_dir in "$VMS_DIR"/*/; do
if [[ -d "$vm_dir" && -f "$vm_dir/mac_hdd_ng.img" ]]; then
local vm_name
vm_name=$(basename "$vm_dir")
local version_file="$vm_dir/.macos-version"
local config_file="$vm_dir/.vm-config"
local display_name="Unknown Version"
local ram_info="8GB"
local cpu_info="4 cores"
if [[ -f "$version_file" ]]; then
display_name=$(cat "$version_file")
fi
# Read resource config if available (in subshell to avoid variable pollution)
if [[ -f "$config_file" ]]; then
# shellcheck source=/dev/null
ram_info="$(source "$config_file"; echo "${RAM_GB:-8}")GB"
# shellcheck source=/dev/null
cpu_info="$(source "$config_file"; echo "${CPU_CORES:-4}") cores"
fi
local disk_size
disk_size=$(du -h "$vm_dir/mac_hdd_ng.img" 2>/dev/null | cut -f1)
echo " $i) $display_name ($vm_name)"
echo " RAM: $ram_info | CPU: $cpu_info | Disk: $disk_size"
vm_list+=("$vm_name")
((i++)) || true
fi
done
shopt -u nullglob
echo ""
echo " N) Create a NEW macOS VM"
echo " D) Delete a VM"
echo " Q) Quit"
echo ""
read -rp "Select an option: " choice
if [[ $choice =~ ^[Nn]$ ]]; then
return 0 # Signal to create new VM
elif [[ $choice =~ ^[Dd]$ ]]; then
delete_vm
# After deletion, show menu again if VMs remain
local remaining
remaining=$(count_existing_vms)
if [[ $remaining -gt 0 ]]; then
show_vm_menu
else
return 0 # No VMs left, prompt to create new
fi
elif [[ $choice =~ ^[Qq]$ ]]; then
exit 0
elif [[ $choice =~ ^[0-9]+$ ]] && (( choice >= 1 && choice < i )); then
local selected_vm="${vm_list[$((choice-1))]}"
launch_existing_vm "$selected_vm"
exit 0
else
print_error "Invalid selection"
show_vm_menu
fi
}
# Launch an existing VM
launch_existing_vm() {
local vm_name="$1"
local vm_dir="$VMS_DIR/$vm_name"
local launcher="$vm_dir/start-macos.sh"
if [[ -x "$launcher" ]]; then
print_info "Launching $vm_name..."
exec "$launcher"
else
print_error "Launcher not found for $vm_name"
exit 1
fi
}
# Delete a VM
delete_vm() {
echo ""
echo -e "${RED}Delete a macOS VM${NC}"
echo ""
echo "Select a VM to delete:"
echo ""
local i=1
local vm_list=()
shopt -s nullglob
for vm_dir in "$VMS_DIR"/*/; do
if [[ -d "$vm_dir" && -f "$vm_dir/mac_hdd_ng.img" ]]; then
local vm_name
vm_name=$(basename "$vm_dir")
local version_file="$vm_dir/.macos-version"
local display_name="Unknown Version"
if [[ -f "$version_file" ]]; then
display_name=$(cat "$version_file")
fi
local disk_size
disk_size=$(du -h "$vm_dir/mac_hdd_ng.img" 2>/dev/null | cut -f1)
echo " $i) $display_name ($vm_name) - Disk: $disk_size"
vm_list+=("$vm_name")
((i++)) || true
fi
done
shopt -u nullglob
echo ""
echo " C) Cancel"
echo ""
read -rp "Select VM to delete: " choice
if [[ $choice =~ ^[Cc]$ ]]; then
return 1 # Cancelled
elif [[ $choice =~ ^[0-9]+$ ]] && (( choice >= 1 && choice < i )); then
local selected_vm="${vm_list[$((choice-1))]}"
local selected_dir="$VMS_DIR/$selected_vm"
local version_file="$selected_dir/.macos-version"
local display_name="$selected_vm"
if [[ -f "$version_file" ]]; then
display_name=$(cat "$version_file")
fi
echo ""
print_warning "You are about to delete: $display_name ($selected_vm)"
print_warning "This will permanently delete:"
echo " - Virtual disk (mac_hdd_ng.img)"
echo " - macOS base image (BaseSystem.img)"
echo " - VM configuration and launcher"
echo " - Desktop menu entry"
echo ""
read -rp "Type 'DELETE' to confirm: " confirm
if [[ "$confirm" == "DELETE" ]]; then
# Remove desktop entry
local desktop_file="$HOME/.local/share/applications/macos-vm-${selected_vm}.desktop"
if [[ -f "$desktop_file" ]]; then
rm -f "$desktop_file"
print_status "Removed desktop entry"
fi
# Remove VM directory
rm -rf "$selected_dir"
print_status "Deleted VM: $selected_vm"
echo ""
# Check if any VMs remain
local remaining
remaining=$(count_existing_vms)
if [[ $remaining -eq 0 ]]; then
print_info "No VMs remaining."
fi
return 0
else
print_info "Deletion cancelled"
return 1
fi
else
print_error "Invalid selection"
return 1
fi
}
# Check if running as root (we don't want that)
check_not_root() {
if [[ $EUID -eq 0 ]]; then
print_error "Please run this script as a normal user, not root."
print_info "The script will ask for sudo when needed."
exit 1
fi
}
# Check CPU virtualization support
check_cpu_support() {
print_info "Checking CPU virtualization support..."
if grep -E '(vmx|svm)' /proc/cpuinfo > /dev/null 2>&1; then
if grep -q 'vmx' /proc/cpuinfo; then
print_status "Intel VT-x supported"
else
print_status "AMD-V supported"
fi
else
print_error "CPU virtualization not supported or not enabled in BIOS"
print_info "Please enable VT-x (Intel) or AMD-V in your BIOS settings"
exit 1
fi
}
# Check if KVM is available
check_kvm() {
print_info "Checking KVM availability..."
if [[ -e /dev/kvm ]]; then
if [[ -r /dev/kvm && -w /dev/kvm ]]; then
print_status "KVM is available and accessible"
else
print_warning "KVM exists but not accessible. Will fix permissions..."
fi
else
print_warning "KVM device not found. Will load modules..."
fi
}
# Install required packages
install_packages() {
print_info "Installing required packages..."
# Official repo packages (installed via pacman)
local pacman_packages=(
qemu-full
libvirt
virt-manager
dnsmasq
edk2-ovmf
swtpm
git
wget
python
python-pip
p7zip
openbsd-netcat
screen
htop
)
# AUR packages (installed via yay)
local aur_packages=(
dmg2img
cdrtools
)
# Check which pacman packages need to be installed
local to_install=()
for pkg in "${pacman_packages[@]}"; do
if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
to_install+=("$pkg")
fi
done
if [[ ${#to_install[@]} -gt 0 ]]; then
print_info "Installing from official repos: ${to_install[*]}"
sudo pacman -S --needed --noconfirm "${to_install[@]}"
print_status "Official repo packages installed"
else
print_status "All official repo packages already installed"
fi
# Check for yay
if ! command -v yay &> /dev/null; then
print_error "yay not found. Please install yay to continue."
print_info "AUR packages needed: ${aur_packages[*]}"
exit 1
fi
# Check which AUR packages need to be installed
local aur_to_install=()
for pkg in "${aur_packages[@]}"; do
if ! pacman -Qi "$pkg" > /dev/null 2>&1; then
aur_to_install+=("$pkg")
fi
done
if [[ ${#aur_to_install[@]} -gt 0 ]]; then
print_info "Installing from AUR: ${aur_to_install[*]}"
yay -S --needed --noconfirm "${aur_to_install[@]}"
print_status "AUR packages installed"
else
print_status "All AUR packages already installed"
fi
}
# Configure user groups
configure_groups() {
print_info "Configuring user groups..."
local groups=("kvm" "libvirt" "input")
local added_groups=()
for group in "${groups[@]}"; do
if ! id -nG "$USER" | grep -qw "$group"; then
sudo usermod -aG "$group" "$USER"
added_groups+=("$group")
fi
done
if [[ ${#added_groups[@]} -gt 0 ]]; then
print_status "Added user to groups: ${added_groups[*]}"
print_warning "You must log out and back in for group changes to take effect."
echo ""
print_info "Options:"
echo " 1) Exit now, relog, then run this script again"
echo " 2) Continue anyway (you'll need to relog before running the VM)"
echo ""
local group_choice
read -rp "Choose (1 or 2) [1]: " -n 1 group_choice
echo
group_choice=${group_choice:-1}
if [[ $group_choice == "1" ]]; then
print_info "Please log out and back in, then run this script again."
exit 0
fi
NEEDS_RELOGIN=true
else
print_status "User already in required groups"
fi
}
# Enable and start services
configure_services() {
print_info "Configuring services..."
# Enable libvirtd
if ! systemctl is-enabled libvirtd > /dev/null 2>&1; then
sudo systemctl enable libvirtd
print_status "Enabled libvirtd service"
fi
if ! systemctl is-active libvirtd > /dev/null 2>&1; then
sudo systemctl start libvirtd
print_status "Started libvirtd service"
fi
print_status "Services configured"
}
# Configure KVM module
configure_kvm() {
print_info "Configuring KVM module..."
# Create modprobe config for KVM
local kvm_conf="/etc/modprobe.d/kvm.conf"
if [[ ! -f "$kvm_conf" ]] || ! grep -q "ignore_msrs=1" "$kvm_conf" 2>/dev/null; then
echo "options kvm ignore_msrs=1" | sudo tee "$kvm_conf" > /dev/null
print_status "Created KVM configuration"
else
print_status "KVM already configured"
fi
# Load KVM module with ignore_msrs
sudo modprobe kvm
echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs > /dev/null
print_status "KVM module configured"
}
# Check IOMMU groups (for passthrough compatibility)
check_iommu() {
print_info "Checking IOMMU configuration..."
# Check if IOMMU is enabled
if [[ ! -d /sys/kernel/iommu_groups ]] || [[ -z "$(ls -A /sys/kernel/iommu_groups 2>/dev/null)" ]]; then
print_warning "IOMMU is not enabled or not available"
echo ""
echo "To enable IOMMU, add to your kernel parameters:"
if grep -q 'vendor_id.*GenuineIntel' /proc/cpuinfo; then
echo " intel_iommu=on iommu=pt"
else
echo " amd_iommu=on iommu=pt"
fi
echo ""
echo "Edit /etc/default/grub or your bootloader config, then reboot."
echo ""
return 1
fi
print_status "IOMMU is enabled"
local show_groups
read -rp "Show IOMMU groups? (useful for GPU/USB passthrough) (y/n) [n]: " -n 1 show_groups
echo
if [[ $show_groups =~ ^[Yy]$ ]]; then
echo ""
echo -e "${BLUE}IOMMU Groups:${NC}"
echo "─────────────────────────────────────────────────────────"
for iommu_group in /sys/kernel/iommu_groups/*/devices/*; do
if [[ -e "$iommu_group" ]]; then
local group_num
group_num=$(echo "$iommu_group" | grep -oP 'iommu_groups/\K[0-9]+')
local device
device=$(basename "$iommu_group")
local desc
desc=$(lspci -nns "$device" 2>/dev/null | cut -d' ' -f2-)
echo -e " Group ${GREEN}$group_num${NC}: $device"
echo " $desc"
fi
done
echo "─────────────────────────────────────────────────────────"
echo ""
print_info "For passthrough, all devices in a group must be passed together"
echo ""
fi
return 0
}
# Configure USB passthrough for a VM
configure_usb_passthrough() {
print_info "USB Passthrough Configuration"
echo ""
echo "You can pass USB devices directly to the macOS VM."
echo "This is useful for iPhones, USB drives, etc."
echo ""
local setup_usb
read -rp "Configure USB passthrough? (y/n) [n]: " -n 1 setup_usb
echo
if [[ ! $setup_usb =~ ^[Yy]$ ]]; then
return
fi
# Check if lsusb is available
if ! command -v lsusb &> /dev/null; then
print_warning "lsusb not found. Installing usbutils..."
sudo pacman -S --needed --noconfirm usbutils
fi
echo ""
echo -e "${BLUE}Available USB devices:${NC}"
echo "─────────────────────────────────────────────────────────"
local i=1
local usb_devices=()
local usb_names=()
while IFS= read -r line; do
local vid pid name
vid=$(echo "$line" | grep -oP 'ID \K[0-9a-f]{4}' || true)
pid=$(echo "$line" | grep -oP 'ID [0-9a-f]{4}:\K[0-9a-f]{4}' || true)
[[ -z "$vid" || -z "$pid" ]] && continue
# Extract device name (everything after the ID)
name=${line#*ID [0-9a-f][0-9a-f][0-9a-f][0-9a-f]:[0-9a-f][0-9a-f][0-9a-f][0-9a-f] }
# Skip root hubs and internal controllers
if [[ "$name" =~ "hub" ]] || [[ "$name" =~ "Host Controller" ]]; then
continue
fi
echo " $i) [$vid:$pid] $name"
usb_devices+=("$vid:$pid")
usb_names+=("$name")
((i++)) || true
done < <(lsusb)
echo "─────────────────────────────────────────────────────────"
echo ""
if [[ ${#usb_devices[@]} -eq 0 ]]; then
print_warning "No passthrough-suitable USB devices found"
return
fi
echo "Enter device numbers to pass through (comma-separated, e.g., 1,3,5)"
echo "Or press Enter to skip."
read -rp "Devices: " selected_devices
if [[ -z "$selected_devices" ]]; then
return
fi
# Parse selected devices and build USB args
USB_PASSTHROUGH_ARGS=""
USB_PASSTHROUGH_DEVICES=()
IFS=',' read -ra selections <<< "$selected_devices"
for sel in "${selections[@]}"; do
sel=$(echo "$sel" | tr -d ' ')
if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel <= ${#usb_devices[@]} )); then
local idx=$((sel - 1))
local vidpid="${usb_devices[$idx]}"
local vid="${vidpid%:*}"
local pid="${vidpid#*:}"
USB_PASSTHROUGH_ARGS+=" -device usb-host,vendorid=0x${vid},productid=0x${pid}"$'\n'
USB_PASSTHROUGH_DEVICES+=("${usb_names[$idx]} ($vidpid)")
print_status "Added: ${usb_names[$idx]}"
fi
done
if [[ -n "$USB_PASSTHROUGH_ARGS" ]]; then
echo ""
print_info "USB devices will be passed to this VM"
print_warning "These devices will be unavailable to the host while VM is running"
fi
}
# Setup GenSMBIOS for iCloud/iMessage compatibility
setup_gensmbios() {
print_info "SMBIOS Configuration (for iCloud/iMessage)"
echo ""
echo "Generating unique SMBIOS data can help with:"
echo " - iCloud sign-in"
echo " - iMessage/FaceTime activation"
echo " - App Store access"
echo ""
local setup_smbios
read -rp "Generate SMBIOS data? (y/n) [n]: " -n 1 setup_smbios
echo
if [[ ! $setup_smbios =~ ^[Yy]$ ]]; then
return
fi
local gensmbios_dir="$INSTALL_DIR/tools/GenSMBIOS"
# Clone GenSMBIOS if not present
if [[ ! -d "$gensmbios_dir" ]]; then
print_info "Downloading GenSMBIOS..."
mkdir -p "$INSTALL_DIR/tools"
git clone --depth 1 https://github.com/corpnewt/GenSMBIOS.git "$gensmbios_dir"
fi
# Determine appropriate Mac model based on macOS version
local mac_model
case "$CURRENT_MACOS_VERSION" in
high-sierra|mojave|catalina)
mac_model="iMac19,1"
;;
big-sur|monterey)
mac_model="iMacPro1,1"
;;
ventura|sonoma|sequoia|tahoe|*)
mac_model="MacPro7,1"
;;
esac
echo ""
echo "Recommended model for $CURRENT_MACOS_DISPLAY_NAME: $mac_model"
read -rp "Use this model? (y/n) [y]: " -n 1 use_default
echo
if [[ ! $use_default =~ ^[Nn]$ ]]; then
SMBIOS_MODEL="$mac_model"
else
echo "Available models: iMac19,1, iMac20,1, iMacPro1,1, MacPro7,1, MacBookPro16,1"
read -rp "Enter model: " SMBIOS_MODEL
SMBIOS_MODEL=${SMBIOS_MODEL:-$mac_model}
fi
# Generate SMBIOS using macserial if available in OSX-KVM
local macserial="$INSTALL_DIR/OpenCore/macserial"
if [[ -x "$macserial" ]]; then
print_info "Generating SMBIOS data for $SMBIOS_MODEL..."
local serial_output
serial_output=$("$macserial" -m "$SMBIOS_MODEL" -g -n 1 2>/dev/null)
if [[ -n "$serial_output" ]]; then
SMBIOS_SERIAL=$(echo "$serial_output" | cut -d'|' -f1 | tr -d ' ')
SMBIOS_MLB=$(echo "$serial_output" | cut -d'|' -f2 | tr -d ' ')
SMBIOS_UUID=$(uuidgen)
echo ""
echo -e "${GREEN}Generated SMBIOS:${NC}"
echo " Model: $SMBIOS_MODEL"
echo " Serial: $SMBIOS_SERIAL"
echo " MLB: $SMBIOS_MLB"
echo " UUID: $SMBIOS_UUID"
echo ""
# Save to VM directory
cat > "$CURRENT_VM_DIR/.smbios" << EOF
SMBIOS_MODEL=$SMBIOS_MODEL
SMBIOS_SERIAL=$SMBIOS_SERIAL
SMBIOS_MLB=$SMBIOS_MLB
SMBIOS_UUID=$SMBIOS_UUID
EOF
print_status "SMBIOS data saved to $CURRENT_VM_DIR/.smbios"
print_info "You'll need to manually apply these in OpenCore Configurator"
SMBIOS_GENERATED=true
else
print_warning "Failed to generate SMBIOS data"
fi
else
print_warning "macserial not found in OSX-KVM"
print_info "Run GenSMBIOS manually: python3 $gensmbios_dir/GenSMBIOS.py"
fi
}
# Configure OpenCore boot arguments
configure_boot_args() {
print_info "Boot Arguments Configuration"
echo ""
echo "Boot arguments customize macOS startup behavior."
echo ""
local setup_bootargs
read -rp "Configure boot arguments? (y/n) [n]: " -n 1 setup_bootargs
echo
if [[ ! $setup_bootargs =~ ^[Yy]$ ]]; then
return
fi
echo ""
echo -e "${BLUE}Common boot arguments:${NC}"
echo "─────────────────────────────────────────────────────────"
echo " 1) -v Verbose boot (see boot messages)"
echo " 2) debug=0x100 Debug mode (don't reboot on panic)"
echo " 3) keepsyms=1 Keep symbols for debugging"
echo " 4) -no_compat_check Skip compatibility check (older Macs)"
echo " 5) amfi_get_out_of_my_way=1 Disable AMFI (for unsigned kexts)"
echo "─────────────────────────────────────────────────────────"
echo ""
echo " Presets:"
echo " D) Debug mode (-v debug=0x100 keepsyms=1)"
echo " N) None (clean boot)"
echo " C) Custom (enter your own)"
echo ""
read -rp "Select options (comma-separated, e.g., 1,2 or D): " bootarg_choice
BOOT_ARGS=""
case "$bootarg_choice" in
[Dd])
BOOT_ARGS="-v debug=0x100 keepsyms=1"
;;
[Nn])
BOOT_ARGS=""
;;
[Cc])
read -rp "Enter boot arguments: " BOOT_ARGS
;;
*)
IFS=',' read -ra selections <<< "$bootarg_choice"
for sel in "${selections[@]}"; do
sel=$(echo "$sel" | tr -d ' ')
case "$sel" in
1) BOOT_ARGS+=" -v" ;;
2) BOOT_ARGS+=" debug=0x100" ;;
3) BOOT_ARGS+=" keepsyms=1" ;;
4) BOOT_ARGS+=" -no_compat_check" ;;
5) BOOT_ARGS+=" amfi_get_out_of_my_way=1" ;;
esac
done
BOOT_ARGS=$(echo "$BOOT_ARGS" | xargs) # Trim whitespace
;;
esac
if [[ -n "$BOOT_ARGS" ]]; then
echo ""
print_status "Boot arguments: $BOOT_ARGS"
# Save to VM directory
echo "BOOT_ARGS=\"$BOOT_ARGS\"" >> "$CURRENT_VM_DIR/.vm-config"
echo ""
print_info "To apply boot arguments:"
echo " 1. Boot into macOS"
echo " 2. Mount the OpenCore EFI partition"
echo " 3. Edit EFI/OC/config.plist"
echo " 4. Find NVRAM > Add > 7C436110... > boot-args"
echo " 5. Add: $BOOT_ARGS"
echo ""
fi
}
# Clone OSX-KVM repository
clone_repository() {
print_info "Setting up OSX-KVM repository..."
if [[ -d "$INSTALL_DIR" ]]; then
print_warning "OSX-KVM directory already exists at $INSTALL_DIR"
local update_choice
read -rp "Do you want to update it? (y/n): " -n 1 update_choice
echo
if [[ $update_choice =~ ^[Yy]$ ]]; then
cd "$INSTALL_DIR" || { print_error "Failed to cd to $INSTALL_DIR"; exit 1; }
if ! git pull --rebase; then
print_warning "Git pull failed, continuing with existing version"
fi
if ! git submodule update --init --recursive; then
print_warning "Submodule update failed, continuing anyway"
fi
print_status "Repository updated"
fi
else
if ! git clone --depth 1 --recursive https://github.com/kholia/OSX-KVM.git "$INSTALL_DIR"; then
print_error "Failed to clone OSX-KVM repository"
exit 1
fi
print_status "Repository cloned to $INSTALL_DIR"
fi
}
# Select and download macOS version
download_macos() {
local version_arg=""
local version_choice=""
local confirmed=false
while [[ "$confirmed" != "true" ]]; do
print_info "macOS Version Selection"
echo ""
echo -e "${BLUE}Available macOS versions:${NC}"
echo ""
echo " Older versions (lighter, faster):"
echo " 1) High Sierra (10.13) - 4GB+ RAM, Penryn CPU"
echo " 2) Mojave (10.14) - 4GB+ RAM, Penryn CPU"
echo " 3) Catalina (10.15) - 4GB+ RAM, Penryn CPU"
echo ""
echo " Modern versions (recommended):"
echo " 4) Big Sur (11) - 8GB+ RAM, Penryn CPU"
echo " 5) Monterey (12) - 8GB+ RAM, Penryn CPU"
echo " 6) Ventura (13) - 8GB+ RAM, Haswell CPU"
echo " 7) Sonoma (14) - 8GB+ RAM, Skylake CPU [RECOMMENDED]"
echo ""
echo " Latest versions (may have issues):"
echo " 8) Sequoia (15) - 8GB+ RAM, Skylake CPU"
echo " 9) Tahoe (26) - 16GB+ RAM, Skylake CPU [BETA]"
echo ""
# Clear any buffered input
read -r -t 0.1 -n 10000 _discard 2>/dev/null || true
read -rp "Select macOS version (1-9) [7]: " version_choice
version_choice=${version_choice:-7}
# Normalize input: trim whitespace and convert to lowercase
version_choice=$(echo "$version_choice" | tr '[:upper:]' '[:lower:]' | xargs)
# Map choice to version name (accept both numbers and names)
case $version_choice in
1|high-sierra|highsierra|sierra)
version_arg="high-sierra" ;;
2|mojave)
version_arg="mojave" ;;
3|catalina)
version_arg="catalina" ;;
4|big-sur|bigsur)
version_arg="big-sur" ;;
5|monterey)
version_arg="monterey" ;;
6|ventura)
version_arg="ventura" ;;
7|sonoma)
version_arg="sonoma" ;;
8|sequoia)
version_arg="sequoia" ;;
9|tahoe)
version_arg="tahoe" ;;
*)
print_warning "Invalid selection '$version_choice', defaulting to Sonoma"
version_arg="sonoma" ;;
esac
# Confirm selection
local display_name
display_name=$(get_macos_display_name "$version_arg")
echo ""
print_info "You selected: $display_name"
read -rp "Is this correct? (y/n) [y]: " confirm
confirm=${confirm:-y}
if [[ $confirm =~ ^[Yy]$ ]]; then
confirmed=true
else
echo ""
fi
done
# Set global version variables
CURRENT_MACOS_VERSION="$version_arg"
CURRENT_MACOS_DISPLAY_NAME=$(get_macos_display_name "$version_arg")
# Prompt for VM name (default to version name)
echo ""
print_info "VM Name"
echo " This identifies your VM. You can have multiple VMs of the same macOS version."
echo " Default: $version_arg"
echo ""
read -rp "Enter VM name [$version_arg]: " vm_name
vm_name=${vm_name:-$version_arg}
# Sanitize VM name (lowercase, replace spaces with dashes)
vm_name=$(echo "$vm_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-_')
# Check if VM already exists and handle name conflicts
CURRENT_VM_NAME="$vm_name"
CURRENT_VM_DIR="$VMS_DIR/$vm_name"
while [[ -d "$CURRENT_VM_DIR" ]]; do
print_warning "A VM named '$vm_name' already exists!"
echo ""
echo " 1) Overwrite existing VM"
echo " 2) Choose a different name"
echo ""
read -rp "Select option (1 or 2): " -n 1 overwrite_choice
echo
if [[ $overwrite_choice == "1" ]]; then
rm -rf "$CURRENT_VM_DIR"
break
else
read -rp "Enter a new VM name: " vm_name
vm_name=$(echo "$vm_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-_')
if [[ -z "$vm_name" ]]; then
vm_name="$version_arg"
fi
CURRENT_VM_NAME="$vm_name"
CURRENT_VM_DIR="$VMS_DIR/$vm_name"
fi
done
# Create VM directory
mkdir -p "$CURRENT_VM_DIR"
print_status "Created VM directory: $CURRENT_VM_DIR"
# Save version info
echo "$CURRENT_MACOS_DISPLAY_NAME" > "$CURRENT_VM_DIR/.macos-version"
cd "$INSTALL_DIR" || { print_error "Failed to cd to $INSTALL_DIR"; exit 1; }
# Check for python3
if ! command -v python3 &> /dev/null; then
print_error "python3 not found. Please install python."
exit 1
fi
# Check that fetch script exists
if [[ ! -f "./fetch-macOS-v2.py" ]]; then
print_error "fetch-macOS-v2.py not found in OSX-KVM repo"
print_info "The repository may be incomplete. Try removing $INSTALL_DIR and re-running."
exit 1
fi
print_info "Downloading $CURRENT_MACOS_DISPLAY_NAME recovery image..."
print_info "This may take a while depending on your internet connection..."
# Clean up any previous download to avoid using cached/wrong version
rm -rf com.apple.recovery.boot 2>/dev/null || true
rm -f BaseSystem.dmg BaseSystem.chunklist 2>/dev/null || true
# Debug: show the exact command being run
print_info "Running: python3 ./fetch-macOS-v2.py -s \"$CURRENT_MACOS_VERSION\""
# Run the fetch script
# Note: The -s shortname flag ignores -o and always outputs to current directory
if ! python3 ./fetch-macOS-v2.py -s "$CURRENT_MACOS_VERSION"; then
print_error "Failed to download macOS recovery image"
exit 1
fi
# Find the downloaded DMG file
# The -s flag downloads to current directory (.), not com.apple.recovery.boot
print_info "Looking for downloaded DMG file..."
local dmg_path=""
# Check current directory first (where -s shortname saves)
if [[ -f "./BaseSystem.dmg" ]]; then
dmg_path="./BaseSystem.dmg"
elif [[ -f "com.apple.recovery.boot/BaseSystem.dmg" ]]; then
dmg_path="com.apple.recovery.boot/BaseSystem.dmg"
else
# Look for any .dmg file in current dir or subdirs
dmg_path=$(find . -maxdepth 2 -name "*.dmg" -type f 2>/dev/null | head -1)
fi
if [[ -z "$dmg_path" || ! -f "$dmg_path" ]]; then
print_error "No DMG file found"
print_info "Contents of current directory:"
ls -la ./*.dmg 2>/dev/null || echo " No .dmg files found"
exit 1
fi
print_status "Found DMG: $dmg_path"
# Verify dmg2img is available
if ! command -v dmg2img &> /dev/null; then
print_error "dmg2img not found. Try: yay -S dmg2img"
exit 1
fi
# Convert DMG to IMG
print_info "Converting $(basename "$dmg_path") to BaseSystem.img..."
if ! dmg2img -i "$dmg_path" "$CURRENT_VM_DIR/BaseSystem.img"; then
print_error "Failed to convert DMG to IMG"
print_info "This can happen if the DMG is corrupted or dmg2img has issues"
print_info "Try: rm -rf com.apple.recovery.boot && re-run the script"
exit 1
fi
# Verify the output file was created
if [[ ! -f "$CURRENT_VM_DIR/BaseSystem.img" ]]; then
print_error "BaseSystem.img was not created"
exit 1
fi
print_status "macOS image ready: $CURRENT_VM_DIR/BaseSystem.img"
}
# Configure VM resources (RAM and CPU)
configure_vm_resources() {
print_info "VM Resource Configuration for $CURRENT_MACOS_DISPLAY_NAME"
# Get system info for recommendations
local total_ram_kb
total_ram_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local total_ram_gb=$((total_ram_kb / 1024 / 1024))
local recommended_ram=$((total_ram_gb / 2))
[[ $recommended_ram -lt 4 ]] && recommended_ram=4
[[ $recommended_ram -gt 16 ]] && recommended_ram=16
local total_cores
total_cores=$(nproc)
local recommended_cores=$((total_cores / 2))
[[ $recommended_cores -lt 2 ]] && recommended_cores=2
[[ $recommended_cores -gt 8 ]] && recommended_cores=8
echo ""
echo "Your system: ${total_ram_gb}GB RAM, ${total_cores} CPU cores"
echo ""
echo -e "${BLUE}RAM Configuration:${NC}"
echo " - Minimum for macOS: 4GB"
echo " - Recommended: 8GB+"
echo " - For development: 16GB+"
echo " - Your system has: ${total_ram_gb}GB"
echo ""
read -rp "RAM for this VM in GB [${recommended_ram}]: " vm_ram
vm_ram=${vm_ram:-$recommended_ram}
# Validate RAM input
if ! [[ "$vm_ram" =~ ^[0-9]+$ ]] || [[ "$vm_ram" -lt 2 ]]; then
print_warning "Invalid RAM value, using ${recommended_ram}GB"
vm_ram=$recommended_ram
fi
if [[ "$vm_ram" -gt "$total_ram_gb" ]]; then
print_warning "Requested RAM exceeds system RAM, using ${recommended_ram}GB"
vm_ram=$recommended_ram
fi
# Convert to MiB for QEMU
CURRENT_VM_RAM=$((vm_ram * 1024))
echo ""
echo -e "${BLUE}CPU Configuration:${NC}"
echo " - Minimum for macOS: 2 cores"
echo " - Recommended: 4 cores"
echo " - For development: 6-8 cores"
echo " - Your system has: ${total_cores} cores"
echo ""
read -rp "CPU cores for this VM [${recommended_cores}]: " vm_cores
vm_cores=${vm_cores:-$recommended_cores}
# Validate cores input
if ! [[ "$vm_cores" =~ ^[0-9]+$ ]] || [[ "$vm_cores" -lt 1 ]]; then
print_warning "Invalid core count, using ${recommended_cores}"
vm_cores=$recommended_cores
fi
if [[ "$vm_cores" -gt "$total_cores" ]]; then
print_warning "Requested cores exceed system cores, using ${recommended_cores}"
vm_cores=$recommended_cores
fi
CURRENT_VM_CORES=$vm_cores
# Calculate threads (use 2 threads per core if hyperthreading likely available)
if [[ $total_cores -ge 4 ]]; then
CURRENT_VM_THREADS=$((vm_cores * 2))
else
CURRENT_VM_THREADS=$vm_cores
fi
echo ""
print_status "VM will use: ${vm_ram}GB RAM, ${vm_cores} CPU cores"
# Save config to VM directory
cat > "$CURRENT_VM_DIR/.vm-config" << EOF
MACOS_VERSION=$CURRENT_MACOS_VERSION
RAM_GB=$vm_ram
RAM_MIB=$CURRENT_VM_RAM
CPU_CORES=$CURRENT_VM_CORES
CPU_THREADS=$CURRENT_VM_THREADS
EOF
}
# Create virtual disk
create_virtual_disk() {
print_info "Virtual Disk Setup for $CURRENT_MACOS_DISPLAY_NAME ($CURRENT_VM_NAME)"
local disk_path="$CURRENT_VM_DIR/mac_hdd_ng.img"
if [[ -f "$disk_path" ]]; then
print_warning "Virtual disk already exists for this VM"
local disk_choice
read -rp "Do you want to create a new one? This will DELETE the existing disk! (y/n): " -n 1 disk_choice
echo
if [[ ! $disk_choice =~ ^[Yy]$ ]]; then
print_status "Keeping existing virtual disk"
return
fi
rm -f "$disk_path"
fi
echo ""
echo "Recommended disk sizes:"
echo " - Minimum: 64GB"
echo " - Recommended: 128GB"
echo " - For development: 256GB+"
echo ""
read -rp "Enter disk size in GB [128]: " disk_size
disk_size=${disk_size:-128}
print_info "Creating ${disk_size}GB virtual disk..."
qemu-img create -f qcow2 "$disk_path" "${disk_size}G"
print_status "Virtual disk created: $disk_path"
}
# Setup Samba shared folder
setup_samba_share() {
print_info "Shared Folder Setup"
echo ""
read -rp "Do you want to set up a shared folder with macOS? (y/n) [y]: " setup_share
setup_share=${setup_share:-y}
if [[ ! $setup_share =~ ^[Yy]$ ]]; then
print_info "Skipping shared folder setup"
return
fi
# Install Samba if needed
if ! pacman -Qi samba > /dev/null 2>&1; then
print_info "Installing Samba..."
sudo pacman -S --needed --noconfirm samba
fi
# Create shared directory
local share_dir="$HOME/macos-shared"
read -rp "Shared folder path [$share_dir]: " custom_share_dir
share_dir=${custom_share_dir:-$share_dir}
mkdir -p "$share_dir"
chmod 755 "$share_dir"
print_status "Created shared folder: $share_dir"
# Backup existing smb.conf if it exists
if [[ -f /etc/samba/smb.conf ]]; then
sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.backup
print_status "Backed up existing smb.conf to smb.conf.backup"
fi
# Create Samba config
print_info "Configuring Samba..."
# Check if smb.conf exists and has content
if [[ -f /etc/samba/smb.conf ]] && [[ -s /etc/samba/smb.conf ]]; then
# Check if macos-shared section already exists
if grep -q '^\[macos-shared\]' /etc/samba/smb.conf; then
print_warning "macos-shared share already exists in smb.conf, skipping"
else
# Append the new share to existing config
print_info "Appending macos-shared to existing Samba config..."
sudo tee -a /etc/samba/smb.conf > /dev/null << EOF
[macos-shared]
comment = macOS VM Shared Folder
path = $share_dir
browseable = yes
read only = no
writable = yes
guest ok = no
valid users = $USER
create mask = 0755
directory mask = 0755
EOF
fi
else
# Create new smb.conf
sudo tee /etc/samba/smb.conf > /dev/null << EOF
[global]
workgroup = WORKGROUP
server string = Samba Server
security = user
map to guest = Bad User
dns proxy = no
[macos-shared]
comment = macOS VM Shared Folder
path = $share_dir
browseable = yes
read only = no
writable = yes
guest ok = no
valid users = $USER
create mask = 0755
directory mask = 0755
EOF
fi
# Set Samba password for user
print_info "Setting Samba password for user '$USER'"
print_info "This can be different from your login password"
sudo smbpasswd -a "$USER"
# Enable and start Samba services
sudo systemctl enable smb nmb
sudo systemctl restart smb nmb
# Get local IP for instructions (try multiple methods for robustness)
local local_ip=""
local_ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[0-9.]+' || true)
[[ -z "$local_ip" ]] && local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
[[ -z "$local_ip" ]] && local_ip=$(ip addr show 2>/dev/null | grep -oP 'inet \K[0-9.]+' | grep -v '127.0.0.1' | head -1)
[[ -z "$local_ip" ]] && local_ip="YOUR_IP_ADDRESS"
print_status "Samba configured successfully"
# Save connection info for later display
SAMBA_SHARE_PATH="$share_dir"
SAMBA_IP="$local_ip"
}
# Get version-specific QEMU configuration
# Returns: cpu_args, net_device, display_args, boot_select, boot_notes
get_version_config() {
local version="$1"
case "$version" in
high-sierra)
# High Sierra (10.13) - oldest supported, needs Penryn CPU
VERSION_CPU='Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='vmxnet3'
VERSION_DISPLAY='-device vmware-svga'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES='High Sierra may take longer to boot. Be patient.'
;;
mojave)
# Mojave (10.14)
VERSION_CPU='Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='vmxnet3'
VERSION_DISPLAY='-device vmware-svga'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES=''
;;
catalina)
# Catalina (10.15)
VERSION_CPU='Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-device vmware-svga'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES=''
;;
big-sur)
# Big Sur (11)
VERSION_CPU='Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES=''
;;
monterey)
# Monterey (12)
VERSION_CPU='Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES=''
;;
ventura)
# Ventura (13) - needs newer CPU model
VERSION_CPU='Haswell-noTSX,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES='Ventura requires more resources. 8GB+ RAM recommended.'
;;
sonoma)
# Sonoma (14) - needs Skylake CPU
VERSION_CPU='Skylake-Client,-hle,-rtm,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES='Sonoma requires 8GB+ RAM and may be slow on first boot.'
;;
sequoia)
# Sequoia (15) - needs Skylake CPU
VERSION_CPU='Skylake-Client,-hle,-rtm,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES='Sequoia is newest. Requires 8GB+ RAM, 16GB recommended.'
;;
tahoe)
# Tahoe (26) - Beta, needs Skylake CPU
VERSION_CPU='Skylake-Client,-hle,-rtm,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES='Tahoe is BETA software. Expect issues. 16GB RAM recommended.'
;;
*)
# Default/fallback - use Penryn for safety
VERSION_CPU='Penryn,kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check'
VERSION_NET='virtio-net-pci'
VERSION_DISPLAY='-display gtk,gl=on -vga virtio'
VERSION_BOOT_SELECT='macOS Base System'
VERSION_BOOT_NOTES=''
;;
esac
}
# Generate a unique SSH port for a VM based on its name
generate_ssh_port() {
# Hash the VM name to get a consistent port between 10022-10999
local hash
# Use full md5 hex and convert to decimal to ensure we have enough digits
hash=$(echo -n "$1" | md5sum | cut -c1-8)
local decimal=$((16#$hash % 978))
local port=$((10022 + decimal))
echo "$port"
}
# Generate a unique MAC address for a VM based on its name
generate_mac_address() {
# Use the VM name to generate consistent last 3 octets
local hash
hash=$(echo -n "$1" | md5sum)
local oct1=${hash:0:2}
local oct2=${hash:2:2}
local oct3=${hash:4:2}
echo "52:54:00:$oct1:$oct2:$oct3"
}
# Create launcher script
create_launcher() {
print_info "Creating launcher script for $CURRENT_MACOS_DISPLAY_NAME..."
local launcher="$CURRENT_VM_DIR/start-macos.sh"
# Generate unique network settings for this VM
local ssh_port
ssh_port=$(generate_ssh_port "$CURRENT_VM_NAME")
local mac_addr
mac_addr=$(generate_mac_address "$CURRENT_VM_NAME")
# Use configured values or defaults
local ram_mib=${CURRENT_VM_RAM:-8192}
local cpu_cores=${CURRENT_VM_CORES:-4}
local cpu_threads=${CURRENT_VM_THREADS:-8}
local ram_gb=$((ram_mib / 1024))
# Get version-specific configuration
get_version_config "$CURRENT_MACOS_VERSION"
cat > "$launcher" << EOF
#!/bin/bash
# $CURRENT_MACOS_DISPLAY_NAME VM Launcher
# VM Name: $CURRENT_VM_NAME
# macOS Version: $CURRENT_MACOS_VERSION
# Resources: ${ram_gb}GB RAM, ${cpu_cores} CPU cores
# SSH Port: $ssh_port (ssh -p $ssh_port localhost)
OSX_KVM_DIR="$INSTALL_DIR"
VM_DIR="$CURRENT_VM_DIR"
cd "\$OSX_KVM_DIR" || { echo "Error: OSX-KVM directory not found"; exit 1; }
# Check if KVM is accessible
if [[ ! -r /dev/kvm || ! -w /dev/kvm ]]; then
echo "Error: Cannot access /dev/kvm"
echo "Try: sudo chmod 666 /dev/kvm"
exit 1
fi
# Set ignore_msrs if not set
if [[ "\$(cat /sys/module/kvm/parameters/ignore_msrs)" != "1" ]]; then
echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs > /dev/null
fi
# Check required files exist
if [[ ! -f "\$VM_DIR/mac_hdd_ng.img" ]]; then
echo "Error: Virtual disk not found at \$VM_DIR/mac_hdd_ng.img"
exit 1
fi
if [[ ! -f "\$VM_DIR/BaseSystem.img" ]]; then
echo "Error: macOS base image not found at \$VM_DIR/BaseSystem.img"
exit 1
fi
# VM Resource Configuration
# Edit these values to change RAM/CPU allocation
ALLOCATED_RAM="$ram_mib" # MiB (${ram_gb}GB)
CPU_SOCKETS="1"
CPU_CORES="$cpu_cores"
CPU_THREADS="$cpu_threads"
REPO_PATH="\$OSX_KVM_DIR"
OVMF_DIR="\$REPO_PATH"
# Unique network settings for this VM
SSH_PORT="$ssh_port"
MAC_ADDR="$mac_addr"
# Display boot information
echo "========================================"
echo " $CURRENT_MACOS_DISPLAY_NAME"
echo "========================================"
echo ""
echo "Resources: \$((ALLOCATED_RAM / 1024))GB RAM, \$CPU_CORES CPU cores"
echo "SSH Port: \$SSH_PORT (after enabling Remote Login in macOS)"
echo ""
echo "----------------------------------------"
echo " FIRST BOOT INSTRUCTIONS"
echo "----------------------------------------"
echo "1. In OpenCore menu, select: '$VERSION_BOOT_SELECT'"
echo "2. Wait for Recovery to load (may take a few minutes)"
echo "3. Open 'Disk Utility' from the menu"
echo "4. Select the QEMU HARDDISK and click 'Erase'"
echo "5. Name: 'Macintosh HD', Format: 'APFS', click 'Erase'"
echo "6. Close Disk Utility, select 'Reinstall macOS'"
echo "7. Follow the installation wizard"
echo ""
echo "AFTER INSTALLATION:"
echo " Select 'Macintosh HD' in OpenCore menu to boot"
echo ""
EOF
# Add version-specific notes if any
if [[ -n "$VERSION_BOOT_NOTES" ]]; then
cat >> "$launcher" << EOF
echo "NOTE: $VERSION_BOOT_NOTES"
echo ""
EOF
fi
# Add USB passthrough info if configured
if [[ -n "$USB_PASSTHROUGH_ARGS" ]]; then
cat >> "$launcher" << EOF
echo "USB Passthrough enabled for:"
EOF
for dev in "${USB_PASSTHROUGH_DEVICES[@]}"; do
cat >> "$launcher" << EOF
echo " - $dev"
EOF
done
cat >> "$launcher" << EOF
echo ""
EOF
fi
cat >> "$launcher" << 'EOF'
# Check for Samba share and display connection info
if grep -q '^\[macos-shared\]' /etc/samba/smb.conf 2>/dev/null; then
SAMBA_IP=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[0-9.]+' || hostname -I 2>/dev/null | awk '{print $1}')
if [[ -n "$SAMBA_IP" ]]; then
echo "----------------------------------------"
echo " SHARED FOLDER"
echo "----------------------------------------"
echo "In macOS: Finder → Go → Connect to Server"
echo "Enter: smb://$SAMBA_IP/macos-shared"
echo "Username: $USER"
echo ""
fi
fi
EOF
cat >> "$launcher" << EOF
echo "Press Ctrl+Alt+G to release mouse from VM"
echo "========================================"
echo ""
# shellcheck disable=SC2054
args=(
-enable-kvm -m "\$ALLOCATED_RAM" -cpu $VERSION_CPU
-machine q35
-usb -device usb-kbd -device usb-tablet
-smp "\$CPU_THREADS",cores="\$CPU_CORES",sockets="\$CPU_SOCKETS"
-device usb-ehci,id=ehci
-device nec-usb-xhci,id=xhci
-global nec-usb-xhci.msi=off
-device isa-applesmc,osk="ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc"
-drive if=pflash,format=raw,readonly=on,file="\$OVMF_DIR/OVMF_CODE_4M.fd"
-drive if=pflash,format=raw,file="\$OVMF_DIR/OVMF_VARS-1920x1080.fd"
-smbios type=2
-device ich9-intel-hda -device hda-duplex
-device ich9-ahci,id=sata
-drive id=OpenCoreBoot,if=none,snapshot=on,format=qcow2,file="\$REPO_PATH/OpenCore/OpenCore.qcow2"
-device ide-hd,bus=sata.2,drive=OpenCoreBoot
-device ide-hd,bus=sata.3,drive=InstallMedia
-drive id=InstallMedia,if=none,file="\$VM_DIR/BaseSystem.img",format=raw
-drive id=MacHDD,if=none,file="\$VM_DIR/mac_hdd_ng.img",format=qcow2
-device ide-hd,bus=sata.4,drive=MacHDD
-netdev "user,id=net0,hostfwd=tcp::\${SSH_PORT}-:22" -device "$VERSION_NET,netdev=net0,id=net0,mac=\${MAC_ADDR}"
EOF
# Add USB passthrough devices if configured
if [[ -n "$USB_PASSTHROUGH_ARGS" ]]; then
echo " # USB Passthrough devices" >> "$launcher"
echo -n "$USB_PASSTHROUGH_ARGS" >> "$launcher"
fi
cat >> "$launcher" << EOF
-monitor stdio
$VERSION_DISPLAY
)
qemu-system-x86_64 "\${args[@]}"
EOF
chmod +x "$launcher"
print_status "Launcher script created: $launcher"
print_info "This VM's SSH port: $ssh_port"
# Show USB passthrough summary
if [[ -n "$USB_PASSTHROUGH_ARGS" ]]; then
print_info "USB passthrough configured for ${#USB_PASSTHROUGH_DEVICES[@]} device(s)"
fi
}
# Create desktop entry
create_desktop_entry() {
print_info "Creating desktop entry for $CURRENT_MACOS_DISPLAY_NAME..."
local desktop_dir="$HOME/.local/share/applications"
mkdir -p "$desktop_dir"
# Use VM name for unique desktop file, display name for menu entry
local desktop_file="$desktop_dir/macos-vm-${CURRENT_VM_NAME}.desktop"
# Detect available terminal emulator
local terminal_cmd
if command -v alacritty &> /dev/null; then
terminal_cmd="alacritty -e"
elif command -v kitty &> /dev/null; then
terminal_cmd="kitty"
elif command -v gnome-terminal &> /dev/null; then
terminal_cmd="gnome-terminal --"
elif command -v konsole &> /dev/null; then
terminal_cmd="konsole -e"
elif command -v xterm &> /dev/null; then
terminal_cmd="xterm -e"
else
# Fallback to using Terminal=true
cat > "$desktop_file" << EOF
[Desktop Entry]
Name=$CURRENT_MACOS_DISPLAY_NAME
Comment=Run $CURRENT_MACOS_DISPLAY_NAME in QEMU/KVM ($CURRENT_VM_NAME)
Exec=$CURRENT_VM_DIR/start-macos.sh
Icon=computer
Terminal=true
Type=Application
Categories=System;Emulator;
EOF
print_status "Desktop entry created: $CURRENT_MACOS_DISPLAY_NAME (using default terminal)"
return
fi
cat > "$desktop_file" << EOF
[Desktop Entry]
Name=$CURRENT_MACOS_DISPLAY_NAME
Comment=Run $CURRENT_MACOS_DISPLAY_NAME in QEMU/KVM ($CURRENT_VM_NAME)
Exec=$terminal_cmd $CURRENT_VM_DIR/start-macos.sh
Icon=computer
Terminal=false
Type=Application
Categories=System;Emulator;
EOF
print_status "Desktop entry created: $CURRENT_MACOS_DISPLAY_NAME"
}
# Print final instructions
print_instructions() {
local ram_gb=$((CURRENT_VM_RAM / 1024))
# Get version-specific configuration
get_version_config "$CURRENT_MACOS_VERSION"
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Installation Complete! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${BLUE}VM Created:${NC} $CURRENT_MACOS_DISPLAY_NAME ($CURRENT_VM_NAME)"
echo -e "${BLUE}Location:${NC} $CURRENT_VM_DIR"
echo -e "${BLUE}Resources:${NC} ${ram_gb}GB RAM, ${CURRENT_VM_CORES} CPU cores"
echo ""
echo -e "${BLUE}To start this VM:${NC}"
echo " $CURRENT_VM_DIR/start-macos.sh"
echo ""
echo -e "${BLUE}Or use the app menu:${NC} '$CURRENT_MACOS_DISPLAY_NAME'"
echo ""
echo -e "${BLUE}To create another VM or manage existing VMs:${NC}"
echo " Press Super+Alt+A for the VM Manager TUI"
echo " Or run: $0"
echo ""
# Check if Samba is configured (either from this session or previously)
local smb_ip="${SAMBA_IP:-}"
local smb_path="${SAMBA_SHARE_PATH:-$HOME/macos-shared}"
# If not set this session, try to detect existing config
if [[ -z "$smb_ip" ]] && grep -q '^\[macos-shared\]' /etc/samba/smb.conf 2>/dev/null; then
smb_ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[0-9.]+' || true)
[[ -z "$smb_ip" ]] && smb_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
[[ -z "$smb_ip" ]] && smb_ip="YOUR_IP"
# Try to get the path from smb.conf
smb_path=$(grep -A5 '^\[macos-shared\]' /etc/samba/smb.conf 2>/dev/null | grep 'path' | awk '{print $3}' || echo "$HOME/macos-shared")
fi
if [[ -n "$smb_ip" ]]; then
echo -e "${BLUE}Shared Folder:${NC}"
echo " Linux folder: $smb_path"
echo " In macOS: Finder → Go → Connect to Server"
echo " Enter: smb://$smb_ip/macos-shared"
echo " Username: $USER"
echo " Password: (the Samba password you set)"
echo ""
fi
echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} FIRST BOOT INSTRUCTIONS${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " 1. In OpenCore boot menu, select: ${GREEN}'$VERSION_BOOT_SELECT'${NC}"
echo " 2. Wait for Recovery to load (may take a few minutes)"
echo " 3. Open 'Disk Utility' from the menu"
echo " 4. Select the QEMU HARDDISK and click 'Erase'"
echo " 5. Name: 'Macintosh HD', Format: 'APFS', click 'Erase'"
echo " 6. Close Disk Utility, select 'Reinstall macOS'"
echo " 7. Follow the installation wizard"
echo " 8. Installation takes 30-60 minutes, VM reboots several times"
echo ""
echo -e " ${GREEN}AFTER INSTALLATION:${NC}"
echo " Select 'Macintosh HD' in OpenCore menu to boot normally"
echo ""
if [[ -n "$VERSION_BOOT_NOTES" ]]; then
echo -e " ${YELLOW}NOTE: $VERSION_BOOT_NOTES${NC}"
echo ""
fi
# Show USB passthrough info if configured
if [[ -n "$USB_PASSTHROUGH_ARGS" ]]; then
echo -e "${BLUE}USB Passthrough:${NC}"
for dev in "${USB_PASSTHROUGH_DEVICES[@]}"; do
echo " - $dev"
done
echo ""
fi
# Show SMBIOS info if generated
if [[ "$SMBIOS_GENERATED" == "true" ]]; then
echo -e "${BLUE}SMBIOS (for iCloud/iMessage):${NC}"
echo " Model: $SMBIOS_MODEL"
echo " Serial: $SMBIOS_SERIAL"
echo " Config: $CURRENT_VM_DIR/.smbios"
echo " (Apply these in OpenCore config.plist after first boot)"
echo ""
fi
# Show boot args if configured
if [[ -n "$BOOT_ARGS" ]]; then
echo -e "${BLUE}Boot Arguments:${NC} $BOOT_ARGS"
echo " (Apply in OpenCore config.plist → NVRAM → boot-args)"
echo ""
fi
echo -e "${YELLOW}Tips:${NC}"
echo " - Press Ctrl+Alt+G to release mouse from VM"
echo " - Be patient - first boot and installation are slow"
echo " - To change RAM/CPU: edit values in start-macos.sh"
echo ""
if [[ "$NEEDS_RELOGIN" == "true" ]]; then
echo -e "${RED}IMPORTANT:${NC} You need to log out and back in before running the VM!"
echo " This is required for the group permissions to take effect."
echo ""
fi
}
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Create the TUI manager script
setup_tui_script() {
print_info "Creating TUI manager script..."
local tui_script="$SCRIPT_DIR/macos-vm-tui.sh"
cat > "$tui_script" << 'TUIEOF'
#!/bin/bash
set -Euo pipefail
# =============================================================================
# macOS VM Manager TUI
# =============================================================================
INSTALL_DIR="$HOME/OSX-KVM"
VMS_DIR="$INSTALL_DIR/vms"
INSTALLER_SCRIPT="INSTALLER_PATH_PLACEHOLDER"
detect_theme_colors() {
local ghostty_conf="$HOME/.config/omarchy/current/theme/ghostty.conf"
ACCENT_COLOR="212"
BORDER_COLOR="240"
SUCCESS_COLOR="42"
WARNING_COLOR="214"
ERROR_COLOR="196"
if [[ -f "$ghostty_conf" ]]; then
local palette_6
palette_6=$(grep "^palette = 6=" "$ghostty_conf" 2>/dev/null | cut -d'=' -f3 | tr -d '#')
[[ -n "$palette_6" ]] && ACCENT_COLOR="$palette_6"
fi
}
center_output() {
local width=${1:-70}
local term_width
term_width=$(tput cols)
local padding=$(( (term_width - width) / 2 ))
[[ $padding -lt 0 ]] && padding=0
while IFS= read -r line; do
printf "%${padding}s%s\n" "" "$line"
done
}
count_vms() {
local count=0
if [[ -d "$VMS_DIR" ]]; then
shopt -s nullglob
for vm_dir in "$VMS_DIR"/*/; do
if [[ -d "$vm_dir" && -f "$vm_dir/mac_hdd_ng.img" ]]; then
((count++)) || true
fi
done
shopt -u nullglob
fi
echo "$count"
}
get_vm_list() {
local vms=()
if [[ -d "$VMS_DIR" ]]; then
shopt -s nullglob
for vm_dir in "$VMS_DIR"/*/; do
if [[ -d "$vm_dir" && -f "$vm_dir/mac_hdd_ng.img" ]]; then
vms+=("$(basename "$vm_dir")")
fi
done
shopt -u nullglob
fi
[[ ${#vms[@]} -gt 0 ]] && printf '%s\n' "${vms[@]}"
}
get_vm_display_name() {
local version_file="$VMS_DIR/$1/.macos-version"
[[ -f "$version_file" ]] && cat "$version_file" || echo "macOS ($1)"
}
get_vm_info() {
local vm_dir="$VMS_DIR/$1"
local config_file="$vm_dir/.vm-config"
local ram="8GB" cores="4" disk_size="N/A"
if [[ -f "$config_file" ]]; then
# Read config in subshell to avoid variable pollution
ram="$(source "$config_file"; echo "${RAM_GB:-8}")GB"
cores="$(source "$config_file"; echo "${CPU_CORES:-4}")"
fi
[[ -f "$vm_dir/mac_hdd_ng.img" ]] && disk_size=$(du -h "$vm_dir/mac_hdd_ng.img" 2>/dev/null | cut -f1)
echo "RAM: $ram | CPU: $cores cores | Disk: $disk_size"
}
show_header() {
clear
echo ""
gum style --foreground "$ACCENT_COLOR" --border double --border-foreground "$BORDER_COLOR" \
--align center --width 60 --padding "1 2" "macOS VM Manager" "QEMU/KVM Virtual Machines" | center_output 64
echo ""
}
show_no_vms() {
show_header
gum style --foreground "$WARNING_COLOR" --align center --width 60 "No macOS VMs found" | center_output 60
echo ""
gum style --faint --align center --width 60 "Would you like to create one?" | center_output 60
echo ""
local choice
choice=$(gum choose "Create New VM" "Exit") || exit 0
[[ "$choice" == "Create New VM" ]] && exec "$INSTALLER_SCRIPT" --create-vm
exit 0
}
show_main_menu() {
local vm_count
vm_count=$(count_vms)
[[ $vm_count -eq 0 ]] && show_no_vms
while true; do
show_header
gum style --foreground "$ACCENT_COLOR" --align center --width 60 "Found $vm_count macOS VM(s)" | center_output 60
echo ""
gum style --faint --align center --width 60 "Launch VMs from the application menu" | center_output 60
echo ""
local choice
choice=$(gum choose "Create New VM" "Delete VM" "View VM Details" "Exit") || exit 0
case "$choice" in
"Create New VM") exec "$INSTALLER_SCRIPT" --create-vm ;;
"Delete VM") select_and_delete_vm; vm_count=$(count_vms) ;;
"View VM Details") select_and_view_vm ;;
"Exit") exit 0 ;;
*) exit 0 ;;
esac
done
}
select_and_delete_vm() {
show_header
gum style --foreground "$ERROR_COLOR" --align center --width 60 "Select VM to Delete" | center_output 60
echo ""
local -a vm_names=()
while IFS= read -r vm_name; do
[[ -z "$vm_name" ]] && continue
vm_names+=("$vm_name")
done < <(get_vm_list)
local -a options=()
for vm_name in "${vm_names[@]}"; do
local disk_size
disk_size=$(du -h "$VMS_DIR/$vm_name/mac_hdd_ng.img" 2>/dev/null | cut -f1)
options+=("$(get_vm_display_name "$vm_name") - Disk: $disk_size")
done
options+=("← Back")
local selected
selected=$(printf '%s\n' "${options[@]}" | gum choose) || return
[[ "$selected" == "← Back" || -z "$selected" ]] && return
for i in "${!options[@]}"; do
if [[ "${options[$i]}" == "$selected" && $i -lt ${#vm_names[@]} ]]; then
local vm_to_delete="${vm_names[$i]}"
echo ""
if gum confirm "Delete $(get_vm_display_name "$vm_to_delete")? This cannot be undone!"; then
rm -f "$HOME/.local/share/applications/macos-vm-${vm_to_delete}.desktop" 2>/dev/null || true
rm -rf "${VMS_DIR:?}/${vm_to_delete:?}"
gum style --foreground "$SUCCESS_COLOR" --align center --width 60 "✓ VM deleted" | center_output 60
sleep 1
fi
return
fi
done
}
select_and_view_vm() {
show_header
gum style --foreground "$ACCENT_COLOR" --align center --width 60 "Select VM to View" | center_output 60
echo ""
local -a vm_names=()
while IFS= read -r vm_name; do
[[ -z "$vm_name" ]] && continue
vm_names+=("$vm_name")
done < <(get_vm_list)
local -a options=()
for vm_name in "${vm_names[@]}"; do
options+=("$(get_vm_display_name "$vm_name")")
done
options+=("← Back")
local selected
selected=$(printf '%s\n' "${options[@]}" | gum choose) || return
[[ "$selected" == "← Back" || -z "$selected" ]] && return
for i in "${!options[@]}"; do
if [[ "${options[$i]}" == "$selected" && $i -lt ${#vm_names[@]} ]]; then
local vm_name="${vm_names[$i]}"
local vm_dir="$VMS_DIR/$vm_name"
local config_file="$vm_dir/.vm-config"
local ram="8" cores="4" version="unknown"
if [[ -f "$config_file" ]]; then
ram="$(source "$config_file"; echo "${RAM_GB:-8}")"
cores="$(source "$config_file"; echo "${CPU_CORES:-4}")"
version="$(source "$config_file"; echo "${MACOS_VERSION:-unknown}")"
fi
local disk_size="N/A"
[[ -f "$vm_dir/mac_hdd_ng.img" ]] && disk_size=$(du -h "$vm_dir/mac_hdd_ng.img" 2>/dev/null | cut -f1)
show_header
gum style --foreground "$ACCENT_COLOR" --border normal --align center --width 60 --padding "1 2" "$(get_vm_display_name "$vm_name")" | center_output 64
echo ""
gum style --align left --width 50 "VM Name: $vm_name" "macOS: $version" "RAM: ${ram}GB" "CPU Cores: $cores" "Disk Size: $disk_size" "Location: $vm_dir" | center_output 50
echo ""
gum style --faint --align center --width 60 "Press Enter to go back" | center_output 60
read -r
return
fi
done
}
command -v gum &>/dev/null || { echo "Error: gum required. Install: sudo pacman -S gum"; exit 1; }
detect_theme_colors
show_main_menu
TUIEOF
# Replace the placeholder with actual installer path
if ! sed -i "s|INSTALLER_PATH_PLACEHOLDER|$SCRIPT_DIR/osx-kvm-installer.sh|g" "$tui_script"; then
print_error "Failed to configure TUI script"
rm -f "$tui_script"
exit 1
fi
chmod +x "$tui_script"
print_status "TUI script created: $tui_script"
}
# Create the TUI launcher script (detects terminal)
setup_tui_launcher() {
print_info "Creating TUI launcher..."
local launcher="$SCRIPT_DIR/launch-macos-tui.sh"
cat > "$launcher" << EOF
#!/bin/bash
# Launcher for macOS VM TUI - detects available terminal
TUI_SCRIPT="$SCRIPT_DIR/macos-vm-tui.sh"
CLASS="org.omarchy.MacosVmTui"
if command -v ghostty &>/dev/null; then
exec ghostty --class="\$CLASS" -e "\$TUI_SCRIPT"
elif command -v kitty &>/dev/null; then
exec kitty --class="\$CLASS" -e "\$TUI_SCRIPT"
elif command -v alacritty &>/dev/null; then
exec alacritty --class "\$CLASS" -e "\$TUI_SCRIPT"
else
notify-send "macOS VM Manager" "No supported terminal found (ghostty, kitty, alacritty)"
exit 1
fi
EOF
chmod +x "$launcher"
print_status "TUI launcher created: $launcher"
}
# Setup Hyprland keybinding for TUI
setup_keybinding() {
local bindings_file="$HOME/.config/hypr/bindings.conf"
if [[ ! -f "$bindings_file" ]]; then
print_warning "Hyprland bindings.conf not found, skipping keybinding setup"
return
fi
# Check if keybinding already exists
if grep -q "org.omarchy.MacosVmTui" "$bindings_file" 2>/dev/null; then
print_status "Keybinding already configured (Super+Alt+A)"
return
fi
print_info "Setting up keybinding (Super+Alt+A)..."
# Append the keybinding configuration
cat >> "$bindings_file" << EOF
# macOS VM Manager TUI
windowrule = match:class org.omarchy.MacosVmTui, float on
windowrule = match:class org.omarchy.MacosVmTui, size 800 600
windowrule = match:class org.omarchy.MacosVmTui, center on
windowrule = match:class org.omarchy.MacosVmTui, pin on
bindd = SUPER ALT, A, macOS VM Manager, exec, $SCRIPT_DIR/launch-macos-tui.sh
# End macOS VM Manager TUI
EOF
print_status "Keybinding added: Super+Alt+A → macOS VM Manager"
}
# Run setup for a new VM (after base system is configured)
setup_new_vm() {
download_macos
configure_vm_resources
create_virtual_disk
setup_samba_share
configure_usb_passthrough
setup_gensmbios
configure_boot_args
create_launcher
create_desktop_entry
print_instructions
}
# Ensure TUI files exist (called on every run, always regenerates to pick up updates)
ensure_tui_files() {
setup_tui_script
setup_tui_launcher
}
# Main installation flow
main() {
print_banner
check_not_root
NEEDS_RELOGIN=false
# Always ensure dependencies are installed first (functions are idempotent)
install_packages
configure_groups
configure_services
configure_kvm
# Now check system capabilities (after packages are installed)
check_cpu_support
check_kvm
# Clone repo if not present
if [[ ! -d "$INSTALL_DIR" ]]; then
echo ""
print_info "Setting up OSX-KVM for the first time..."
echo ""
check_iommu # Optional: check IOMMU for passthrough capabilities
clone_repository
fi
# Create VMs directory
mkdir -p "$VMS_DIR"
# Ensure TUI files exist
ensure_tui_files
setup_tui_script
setup_tui_launcher
setup_keybinding
# Check for legacy installation and migrate if needed
if check_legacy_installation; then
migrate_legacy_installation
fi
# Installation complete - direct user to TUI
echo ""
print_status "Installation complete!"
echo ""
print_info "To create and manage macOS VMs:"
echo " - Press Super+Alt+A to open the VM Manager"
echo " - Or run: $SCRIPT_DIR/launch-macos-tui.sh"
echo ""
if [[ "$NEEDS_RELOGIN" == "true" ]]; then
print_warning "Please log out and back in for group permissions to take effect."
echo ""
fi
}
# Create VM mode (called from TUI)
create_vm_mode() {
print_banner
check_not_root
check_cpu_support
check_kvm
# Ensure VMS_DIR exists
mkdir -p "$VMS_DIR"
setup_new_vm
}
# Parse arguments and run
case "${1:-}" in
--create-vm)
create_vm_mode
;;
*)
main "$@"
;;
esac