macOS Tahoe VM images (OpenCore/QEMU), Apple-ID compatible, VNC
Add a macOS image pipeline mirroring the Windows one: unattended install,
generalization and user creation, driven end to end in QEMU on a KVM host.
lib/images/macos:
- makeOpenCore: OSX-KVM OpenCore ESP with a config.plist rewritten per image —
SMBIOS model + serial/MLB (macserial) + UUID + ROM=en0 MAC (built-in NIC pinned
to PciRoot(0x0)/Pci(0x12,0x0)) for Apple ID / iMessage / App Store; boot disk;
OpenCore self-entry hidden. ident.nix derives MAC+UUID from a seed so the NixOS
module and CLI know the NIC MAC at eval time.
- makeImage: one QEMU session driven by vm-driver.py (QMP + screenshot settle
detection + OCR of the menu bar) — boots the recovery via OpenCore, opens
Terminal (Ctrl-F2 -> Utilities -> Terminal), types the bootstrap command;
vmix-install.sh erases the disk as APFS, lays down the host-extracted installer
app skeleton + a byte-exact SharedSupport.dmg (raw disk mapped to that byte range
of the pkg, dd'd in — Recovery's xar truncates an 18 GB member), runs
startosinstall with the vmix agent pkg. OpenCore is then copied into the image's
ESP so it boots standalone with OVMF.
- customizeImage / templates: boot the image with a FAT-then-HFS+ VMIX volume; the
vmix agent (LaunchDaemon) runs a script as root, records status and powers off —
the macOS counterpart of Windows Audit Mode. generalize creates the admin user +
auto-login (kcpassword), suppresses Setup Assistant, sets hostname/timezone,
grows APFS, and assigns a fresh SMBIOS identity. Templates: noUpdates,
performance, remoteAccess (ssh + screen sharing).
- fetchRecovery: Apple recovery BaseSystem, retried until the pinned Tahoe build
(osrecovery load-balances Sequoia/Tahoe during the rollout). makeAgentPkg builds
a distribution flat pkg on Linux (xar+bom+cpio) for startosinstall --installpackage.
CLI: vmix build/copy/run for macOS (run --macos --vnc, reads the image's MAC from
its ESP), and a `vmix macserial` helper. NixOS module: disks.os.file carrying
_vmixOsType="macos" auto-enables the macOS QEMU profile (AppleSMC+OSK, Skylake
CPU spoof, AHCI system disk, VMware SVGA, pinned NIC); macos.{enable,cpu,mac}.
Status: proven through the installer prepare phase (SharedSupport.dmg mounts,
version 26.6.2 read, SU catalog loads). Two blockers remain, documented in
lib/images/macos/README.md: (1) startosinstall's OSISVerifyBaseSystemOperation
rejects the byte-identical plain-UDIF SharedSupport as "pkgdmg missing a footer"
in this Tahoe recovery/VM; (2) Apple's CDN unreliably serves the Tahoe recovery
during rollout (self-hosting the verified BaseSystem is the robust fix).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
This commit is contained in:
parent
6a62a649bd
commit
242e48a5fc
35 changed files with 1842 additions and 36 deletions
|
|
@ -77,13 +77,23 @@ let
|
|||
|
||||
hasOsDisk = vmCfg.disks.os.file != null;
|
||||
|
||||
# Auto-detect Windows from _vmixOsType marker on the disk image
|
||||
isWindows = vmCfg.windows.enable || (hasOsDisk && (vmCfg.disks.os.file._vmixOsType or "linux") == "windows");
|
||||
# Auto-detect Windows / macOS from _vmixOsType marker on the disk image
|
||||
osType = if hasOsDisk then vmCfg.disks.os.file._vmixOsType or "linux" else "linux";
|
||||
isWindows = vmCfg.windows.enable || osType == "windows";
|
||||
isMacos = vmCfg.macos.enable || osType == "macos";
|
||||
macosMac = if vmCfg.macos.mac != null then vmCfg.macos.mac
|
||||
else if hasOsDisk then vmCfg.disks.os.file.macAddress or "52:54:00:c9:18:27"
|
||||
else "52:54:00:c9:18:27";
|
||||
# OpenCore marks this PCI slot built-in (en0)
|
||||
macosNicPlacement = ",bus=pcie.0,addr=${vmixLib.macos.qemu.nicAddr}";
|
||||
cpuArg = if isMacos && vmCfg.cpu.model == "host" then vmCfg.macos.cpu
|
||||
else "${vmCfg.cpu.model}${optionalString (vmCfg.cpu.hideVirtualized && !isMacos) ",kvm=off,hv_vendor_id=1234567890ab,-hypervisor"}";
|
||||
# Interrupt remapping in the virtual IOMMU only works on a split irqchip,
|
||||
# so viommu wins over the full in-kernel irqchip hideVirtualized asks for.
|
||||
# (macOS keeps the default irqchip: hideVirtualized does not apply to it.)
|
||||
machineIrqchipArg =
|
||||
if vmCfg.pci.viommu.enable then ",kernel-irqchip=split"
|
||||
else optionalString vmCfg.cpu.hideVirtualized ",kernel_irqchip=on";
|
||||
else optionalString (vmCfg.cpu.hideVirtualized && !isMacos) ",kernel_irqchip=on";
|
||||
# Functions of one physical device have to reach the guest as functions
|
||||
# of one device too. Giving each address its own root port splits a GPU
|
||||
# from its own HDMI audio, and Navi cannot then reset or power-manage
|
||||
|
|
@ -108,9 +118,9 @@ let
|
|||
'';
|
||||
};
|
||||
|
||||
# Windows VMs: use disk image as-is (customization done at image build time)
|
||||
# Windows/macOS VMs: use disk image as-is (customization done at image build time)
|
||||
storeImage = if !hasOsDisk then null
|
||||
else if isWindows then vmCfg.disks.os.file
|
||||
else if isWindows || isMacos then vmCfg.disks.os.file
|
||||
else linuxOsImage;
|
||||
|
||||
# When persist = true, QEMU needs a mutable disk outside /nix/store.
|
||||
|
|
@ -208,7 +218,7 @@ let
|
|||
-m ${toString vmCfg.mem.size} \
|
||||
${optionalString vmCfg.mem.balloon "-device virtio-balloon-pci"} \
|
||||
-smp cores=${toString vmCfg.cpu.cores} \
|
||||
-cpu ${vmCfg.cpu.model}${optionalString vmCfg.cpu.hideVirtualized ",kvm=off,hv_vendor_id=1234567890ab,-hypervisor"} \
|
||||
-cpu ${cpuArg} \
|
||||
-machine type=${vmCfg.pc.type}${machineIrqchipArg} \
|
||||
${optionalString vmCfg.bios.efi "-bios ${pkgs.OVMF.fd}/FV/OVMF.fd"} \
|
||||
${optionalString vmCfg.bios.tpm "-chardev socket,id=chrtpm,path=/tmp/mytpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"} \
|
||||
|
|
@ -218,7 +228,13 @@ let
|
|||
-device qemu-xhci -device usb-tablet \
|
||||
-global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \
|
||||
''} \
|
||||
${optionalString hasOsDisk "-drive file=${osDiskPath},format=qcow2,if=virtio${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"}"} \
|
||||
${# macOS: AppleSMC + OSK, USB keyboard/tablet, AHCI system disk, VMware SVGA (no SPICE display device)
|
||||
optionalString isMacos ''
|
||||
${vmixLib.macos.qemu.deviceArgs} ${optionalString (!vmCfg.spice.enable) vmixLib.macos.qemu.vgaArgs} \
|
||||
''} \
|
||||
${optionalString hasOsDisk (if isMacos
|
||||
then "-drive id=os,if=none,file=${osDiskPath},format=qcow2${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"} -device ide-hd,bus=sata.0,drive=os"
|
||||
else "-drive file=${osDiskPath},format=qcow2,if=virtio${optionalString (vmCfg.disks.os.persist == false) ",snapshot=on"}")} \
|
||||
${optionalString (vmCfg.disks.iso.file != null) "-drive file=${toString vmCfg.disks.iso.file},media=cdrom,readonly=on"} \
|
||||
${concatMapStrings (diskCfg: ''
|
||||
-drive file=${toString diskCfg.file},format=${diskCfg.format},if=${vmCfg.disks.bus} \
|
||||
|
|
@ -228,12 +244,12 @@ let
|
|||
'') vmCfg.shares)} \
|
||||
${optionalString cfg.networks.user.enable "
|
||||
-netdev user,id=user \
|
||||
-device ${vmCfg.nicModel},netdev=user \
|
||||
-device ${vmCfg.nicModel},netdev=user${optionalString isMacos ",mac=${macosMac}${macosNicPlacement}"} \
|
||||
"} \
|
||||
${concatMapStrings (tapCfg: ''
|
||||
-device ${vmCfg.nicModel},netdev=lan-${tapCfg.name},mac=${tapCfg.mac} \
|
||||
${concatStrings (imap1 (i: tapCfg: ''
|
||||
-device ${vmCfg.nicModel},netdev=lan-${tapCfg.name},mac=${tapCfg.mac}${optionalString (isMacos && i == 1 && !cfg.networks.user.enable) macosNicPlacement} \
|
||||
-netdev tap,id=lan-${tapCfg.name},ifname=${tapCfg.iface},script=no,downscript=no \
|
||||
'') allTaps} \
|
||||
'') allTaps)} \
|
||||
${concatStrings (imap1 (i: macvtap: ''
|
||||
-device ${vmCfg.nicModel},netdev=macvtap-${macvtap.name},mac=$(ip l show ${macvtap.iface} | awk '/link\/ether/{print $2}') \
|
||||
-netdev tap,id=macvtap-${macvtap.name},fd=${toString (i+2)} ${toString (i+2)}<>/dev/tap$(ip l show ${macvtap.iface} | awk -F':' '/${macvtap.iface}/{print $1}') \
|
||||
|
|
|
|||
|
|
@ -249,6 +249,24 @@ with lib;
|
|||
};
|
||||
};
|
||||
|
||||
macos = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable macOS QEMU flags (OpenCore/AppleSMC, Skylake CPU spoof, AHCI disk, pinned NIC). Auto-enabled when disks.os.file carries _vmixOsType = \"macos\" metadata.";
|
||||
};
|
||||
cpu = mkOption {
|
||||
type = types.str;
|
||||
default = vmixLib.macos.qemu.defaultCpu;
|
||||
description = "QEMU -cpu string used for macOS VMs when cpu.model is \"host\".";
|
||||
};
|
||||
mac = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "MAC address of en0. Defaults to the image's macAddress (must match OpenCore's ROM for Apple ID / iMessage).";
|
||||
};
|
||||
};
|
||||
|
||||
tpm = {
|
||||
stateDir = mkOption {
|
||||
type = types.str;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue