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>
2179 lines
71 KiB
Bash
Executable file
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
|