The base image (macos.images.tahoe.upstream) now installs and boots end to end with no dependency on Apple's servers — proven on the KVM host: the finished qcow2 boots standalone (OpenCore from its own ESP) to the macOS 26.6.2 loginwindow. Install driving (vm-driver.py, screenshot + OCR over QMP): - map the whole InstallAssistant.pkg as a raw disk and dd it byte-exact into the app as SharedSupport.dmg (it is a pkgdmg: xar + koly footer — the bare xar member fails startosinstall with "pkgdmg is missing a footer") - offline install: no NIC + /etc/hosts blackhole of Apple install/verify endpoints so startosinstall's calls fail fast instead of hanging (SecureBootModel=Disabled allows the sealed-volume install offline) - keep the recovery display awake with a tiny mouse jiggle; coarse settle fingerprint so the cursor is not seen as a change - guest watchdog re-erases/retries a startosinstall attempt that stalls or runs too long (prepare is intermittently slow) - disk-aware boot watchdog: QMP system_reset only when the screen is dark AND the disk is idle (never interrupts a slow-but-working boot); recovery-restart if a post-prepare reboot lands back on recovery - detect the bright loginwindow and power the VM down (install complete); a black + disk-idle screen is treated as a completed halt - copy OpenCore into the image ESP so it boots standalone recovery.file pins a content-addressed local BaseSystem.dmg (Apple's CDN load-balances Sequoia/Tahoe during the rollout). fetchRecovery retries to the pinned hash when used instead. Not yet done: .generalize (user creation) — the first-boot agent LaunchDaemon is blocked by Ventura+ Background Task Management on headless boots; next step is offline user injection from the agent pkg postinstall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF |
||
|---|---|---|
| .. | ||
| guest | ||
| helpers | ||
| tahoe | ||
| templates | ||
| default.nix | ||
| README.md | ||
| upstream.json | ||
vmix macOS images
Unattended macOS (Tahoe / 26) VM images, built the same way as the Windows
images: makeImage installs the OS once, templates customize it by booting it,
.generalize creates the user and seals the image.
vmix build --image macos.images.tahoe.basic \
--generalize username=sagar,password=secret,hostname=MAC,timezone=Europe/Zurich
vmix run ./result --macos --vnc :10 --mem 8192 # VNC on port 5910
How it works
| step | what happens |
|---|---|
fetchRecovery |
BaseSystem.dmg from Apple's recovery servers (fixed-output, pinned by sha256) |
installerPayload |
takes the App Store InstallAssistant.pkg (18 GB, pinned) apart on Linux: the app skeleton (pbzx/cpio) and the byte offset of SharedSupport.dmg |
makeOpenCore |
OSX-KVM's OpenCore ESP with a config.plist rewritten for this image: SMBIOS model, serial + MLB (macserial), UUID and ROM = NIC MAC (derived from a seed), NIC marked built-in |
makeImage |
one QEMU session: Recovery boots via OpenCore → vm-driver.py opens Terminal with keystrokes (Ctrl-F2 menu navigation, screen-settle detection + OCR of the menu bar) and types sh /Volumes/VMIX/run.sh → vmix-install.sh erases the disk, rebuilds Install macOS Tahoe.app (skeleton + SharedSupport.dmg copied from a raw disk mapped straight out of the pkg), runs startosinstall --installpackage vmix-agent.pkg → installer reboots through its phases → first boot runs the vmix agent which powers off. OpenCore is then copied into the image's EFI partition, so it boots standalone with OVMF |
customizeImage |
boots the image with a FAT volume VMIX; the agent (LaunchDaemon ch.vmix.agent) runs vmix-run.sh as root, writes vmix-run.status/.log back and shuts down |
templates.generalize |
user (admin) + auto-login (/etc/kcpassword), Setup Assistant suppressed, hostname, timezone, no sleep, APFS grown to the disk, then the agent removes itself; a fresh SMBIOS identity is written to the ESP |
The vmix agent replaces Windows' Audit Mode RunOnce; .AppleSetupDone replaces
the OOBE unattend. Everything on the host side runs inside __noChroot
derivations (KVM + /tmp), exactly like the Windows builders.
Generalize options
username password fullName autoLogon hostname locale timezone delayOobeRun
as for Windows (bgColor is accepted but ignored), plus the SMBIOS identity:
model serial mlb uuid mac seed. Anything unset is generated: serial/MLB by
macserial (random per build), MAC and UUID deterministically from seed
(default hostname-username). vmix macserial --model MacPro7,1 prints a
ready-to-paste set.
delay-oobe-run=true creates no user and re-arms Setup Assistant for the first
real boot.
Apple ID / iMessage
The image satisfies what Dortania lists for iServices: unique serial + MLB for a
Tahoe-supported model (MacPro7,1 by default; iMac20,1/2,
MacBookPro16,x also work), SystemUUID, ROM equal to en0's MAC, and en0 marked
built-in (the NIC is pinned to PciRoot(0x0)/Pci(0x12,0x0)). The NixOS module
and vmix run --macos use the MAC recorded in the image (EFI/vmix/vmix.json).
Give each deployed VM its own generalized image (different seed, or explicit
serial=/mlb=) — two VMs with the same identity will be blocked.
Runtime
vmix run <qcow2> --macos [--vnc :N] [--mac ..]- NixOS module:
disks.os.file = vmixLib.macos.images.tahoe.basic.generalize {...}is auto-detected (_vmixOsType = "macos"): Skylake-Client CPU spoof, AppleSMC, USB keyboard/tablet, AHCI system disk, VMware SVGA, pinned NIC with the image's MAC.macos.cpu,macos.mac,macos.enableoverride the defaults. vmix copywrites the image to a disk but cannot grow APFS from Linux (diskutil apfs resizeContainer disk0s2 0in macOS afterwards).
Debugging a build
Screenshots (NNN-<state>.png), driver.log and the QMP socket of every VM
session are in /tmp/vmix-macos/<image name>/ on the build host. The guest logs
(install.log, vmix-run.log, vmix-agent.log) are printed at the end of the
build. Pass vncDisplay = ":10" to makeImage/customizeImage (or
--generalize vncDisplay=:10) to watch live; with a DISPLAY an SDL window
is used as for Windows.
Updating pins (upstream.json)
- installer: URL + SRI hash of a newer
InstallAssistant.pkg(nix store prefetch-file --name InstallAssistant.pkg <url>; Mr. Macintosh's database lists Apple's URLs) - recovery: Apple serves the current build for the board id, so the sha256 changes with each point release — copy the "got:" hash from the failed build
- opencore: OSX-KVM
OpenCore.qcow2at a commit; OpenCorePkg release zip (macserial/ocvalidate)
Known limits
- The Recovery bootstrap depends on keyboard navigation of the Recovery UI
(Ctrl-F2 → Utilities → Terminal). It self-corrects with screenshots + OCR and
falls back to a blind sequence, but a Recovery UI change would need
vm-driver.pyadjusted. - Hosts must run KVM with an AVX2-capable CPU (Intel or AMD; the guest sees a
Skylake).
sandbox = relaxedand thekvmsystem feature, as for Windows. - Software updates inside the VM are disabled by the
noUpdatestemplate (OTA updates in a VM need the RestrictEvents kext).
Current status (2026-09-09): working offline install
macos.images.tahoe.upstream builds a bootable, installed macOS Tahoe 26.6.2
qcow2 fully offline on the KVM host — no dependency on Apple's servers at build
time, just the pinned local InstallAssistant.pkg and BaseSystem.dmg. The
finished image boots standalone (OpenCore from its own ESP) to the macOS
loginwindow. Serial/MLB/UUID/ROM are per-image for Apple ID / iMessage.
How the install is driven (vm-driver.py, all by screenshot + OCR over QMP):
- The whole
InstallAssistant.pkgis mapped as a raw disk (it is a "pkgdmg": xar + koly footer) anddd'd byte-exact into the app asSharedSupport.dmg— extracting the bare xar member fails startosinstall with "pkgdmg missing a footer". - No NIC during install +
/etc/hostsblackhole of Apple's install/verify endpoints, sostartosinstall's network calls fail fast instead of hanging — offline prepare, no external dependency.SecureBootModel=Disabledlets the sealed volume install without online personalization. - The recovery display is kept awake with a tiny mouse jiggle (a lone keypress does not reset display sleep, and the sleeping display swallows the menu-nav keystrokes); the settle detector uses a coarse fingerprint so the jiggling cursor is not seen as a screen change.
- startosinstall prepare is intermittently slow/stalls; a guest watchdog kills and re-erases/retries an attempt that stalls or runs > 9 min.
- First boot in QEMU intermittently hangs at the Apple logo; a disk-aware watchdog
(
--progress-file) issues a QMPsystem_resetonly when the screen is dark AND the disk is idle, so a slow-but-working boot is never interrupted. - The install reaching the (bright) loginwindow is detected by brightness (the
faint gray "password" text does not OCR) and the driver powers the VM down —
the image is installed. macOS
shutdown -h nowhalts to black without an ACPI power-off, so a black+disk-idle screen is also treated as a completed halt. - OpenCore is then copied into the image's own ESP so it boots standalone with OVMF.
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 (same path
from the same bytes). Set recovery.sha256 and remove recovery.file to fetch it
from Apple instead (subject to the CDN rollout).
Not yet done: generalize / user creation
The base image installs and boots to loginwindow. .generalize (user creation,
auto-login, hostname) relies on the vmix agent LaunchDaemon running on first boot,
but macOS Ventura+ Background Task Management does not auto-run a headless
third-party daemon, and neither the pkg launchctl bootstrap (installer domain
only) nor a cron @reboot reliably triggered it. The robust next step is to inject
the user record + settings offline from the agent pkg's postinstall (which runs as
root on the target during install), instead of a first-boot daemon.