The OSX-KVM ESP ships Lilu 1.6.8 / VirtualSMC 1.3.3 / WhateverGreen 1.6.7,
which disable themselves on macOS 26; Apple's SMC driver then runs on QEMU's
isa-applesmc stub and the restart path panics (SMCWDT smcWriteKey
kSMCBadCommand → nested panic after MACH Reboot). Overlay pinned current
releases (upstream.json opencore.kexts) and drop isa-applesmc: with the stub
present VirtualSMC steps aside ("multiple devices present"); alone it carries
the OSK and reboots work (PE restart test: 10 s, clean). RestrictEvents with
revpatch=memtab silences MacPro7,1's "Memory Modules Misconfigured".
- makeOpenCore: kext overlay + Kernel.Add entries for overlaid kexts,
--memory-mb (4 DIMMs), bootArgs default revpatch=memtab
- qemu.nix: deviceArgsFor { appleSmc } (default false); cli: --applesmc for
images built before this change
- vm-driver: reboot-death detection (reset 60 s after a guest reboot request
that never comes back), panics wait for XNU's own auto-reboot, debug dir
works across nixbld users
- generalize: QEMU USB keyboard declared ANSI (no Keyboard Setup Assistant)
- README: architecture, reliability handling, debugging
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
|
||
|---|---|---|
| .. | ||
| guest | ||
| helpers | ||
| tahoe | ||
| templates | ||
| tools | ||
| default.nix | ||
| README.md | ||
| upstream.json | ||
macOS images (Tahoe 26)
Pre-installed, Apple-ID-capable macOS VM images built the same way as the
Windows ones: makeImage (unattended install) → templates → .generalize
(user, hostname, fresh SMBIOS identity). Runs on QEMU/KVM with OpenCore.
vmix build --image macos.images.tahoe.basic --generalize username=sagar,password=secret,hostname=MAC
vmix run ./result --macos --vnc :10 --mem 8192
Nix: macos.images.tahoe.{pe,upstream,basic,remote} and
<image>.generalize { username; password; hostname; timezone; locale; seed; … }.
How it works: the vmix "PE"
Apple's Recovery (BaseSystem.dmg, a plain journaled HFS+ volume) with one
LaunchDaemon added (makeRecoveryPE): at boot it mounts a VMIX volume and
runs run.sh from it as root, records the exit status and powers off. That is
the whole automation surface — the equivalent of Windows PE + Autounattend:
- no GUI is driven: no OCR, no keystrokes, no screen layouts to learn per macOS version; the hook is a launchd plist, stable across releases (same idea as AutoNBI/Imagr NetBoot images).
- observable: the guest prints
VMIX-*markers to/dev/console, which the build reads from QEMU's serial log (boot-args serial=3 -v). Kernel panics and reboots show up there too. Screenshots are still taken for debugging. - offline: no NIC during the install, and the guest blackholes Apple's
install/verify endpoints so
startosinstallnever waits on the network. The only inputs are the pinnedInstallAssistant.pkgandBaseSystem.dmg. - everything else happens offline from the PE too: templates and generalize
mount the image's Data volume (rw) and System volume (ro) and edit them
(
dscl -ffor users,plutilfor preferences) — the installed macOS is never booted for customization, so nothing depends on launchd/BTM approval, first-boot agents or auto-login inside the guest. One PE boot ≈ 30 s.
Pipeline
makeRecoveryPE— BaseSystem.dmg → raw HFS+ image +ch.vmix.pedaemon.makeImage— QEMU with: OpenCore boot disk (build variant with serial console), the PE, the empty target disk, the VMIX volume (vmix-install.sh, installer app skeleton) and the wholeInstallAssistant.pkgmapped as a raw disk. The guest script erases the target as APFS, unpacks the app anddds the pkg into it asSharedSupport.dmg(it is a "pkgdmg": xar + koly footer; the bare xar member fails with "pkgdmg is missing a footer"), then runsstartosinstall, which reboots itself through the install phases. The installed system's first boot ends at the loginwindow: the driver detects the bright screen and powers the VM down. OpenCore is then copied into the image's own ESP so it boots with plain OVMF.customizeImage— boots the PE with the image attached (OpenCoreScanPolicyrestricted to HFS+ on SATA, so only the PE can boot) and runs the template script with$SYS/$DATAmounted.pe-lib.shhas the helpers.templates/generalize.nix— user (dscl, admin, home from the user template), auto-login (kcpassword), Setup Assistant suppression, hostname, locale, timezone, keyboard type, container resize, fresh SMBIOS via a new OpenCore ESP (serial/mlbfrom macserial, MAC + UUID fromseed).
Recovery source
recovery.file in upstream.json points at a content-addressed store path for
the verified Tahoe BaseSystem.dmg (Apple's CDN load-balances Sequoia/Tahoe
during the rollout, so a plain fetch is non-deterministic). Reproduce it on any
host with nix store add-path --name macos-tahoe-BaseSystem.dmg BaseSystem.dmg.
Drop recovery.file to fetch from Apple instead (fetchRecovery retries until
the pinned hash matches).
Reliability
Things QEMU does intermittently, and what handles each (all in vm-driver.py
and vmix-install.sh; every event is logged with a reason):
startosinstallprepare stalls or crawls — the guest kills and retries it on a freshly erased target (free-space watchdog + time cap).- the installer comes back to the PE instead of the install phase — the PE counts boots and simply re-runs the install (max 3).
- the installed system hangs at the Apple logo on first boot — a
system_resetis issued only when the screen is dark and frozen and disk and serial console are idle, so a slow-but-working boot is never interrupted. - macOS
shutdown -hhalts to a black screen without an ACPI power-off — an idle black screen counts as a completed halt. - a kernel panic (seen on the serial console) resets the VM.
- a wedged run fails at the 4 h timeout instead of hanging.
tools/soak.sh <flake> macos.images.tahoe.upstream 3 rebuilds an image N
times and tabulates outcome, duration, boots, resets, panics and retries.
Debugging
/tmp/vmix-macos/<name>/ on the build host: driver.log, serial.log
(kernel + VMIX-* markers), periodic PNG screenshots, qmp.sock.
vmix-run.log / system-install.log from the VMIX volume are printed at the
end of the build. Add vncDisplay = ":10" to watch.
QEMU profile
helpers/qemu.nix: q35, Skylake-Client CPU spoof (works on AMD),
AppleSMC with the OSK, XHCI keyboard/tablet, AHCI disks, VMware SVGA,
virtio-net pinned to PciRoot(0x0)/Pci(0x12,0x0) so OpenCore marks it built-in
(en0, required for Apple ID / iMessage). SMBIOS MacPro7,1 with four DIMMs
described (avoids the "Memory Modules Misconfigured" warning).
OpenCore comes from OSX-KVM's proven ESP, with Lilu / VirtualSMC /
WhateverGreen replaced by current releases (upstream.json → opencore.kexts):
the versions OSX-KVM ships disable themselves on macOS 26, and without
VirtualSMC the guest's restart path panics on QEMU's SMC stub
(SMCWDT smcWriteKey kSMCBadCommand, nested panic after MACH Reboot).
For the same reason QEMU's isa-applesmc is not used any more: its presence
makes VirtualSMC step aside ("multiple devices present"); VirtualSMC carries
the OSK itself. Images built before this change still need the stub:
vmix run --macos --applesmc. RestrictEvents (revpatch=memtab) silences
MacPro7,1's "Memory Modules Misconfigured" at login.