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
|
||
|---|---|---|
| .. | ||
| 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-08)
Everything up to and including the macOS Installer's prepare phase is working and
proven end to end on the root@daku.home KVM host:
- OpenCore ESP per-image SMBIOS (serial/MLB via macserial, ROM=MAC, UUID), boot
disk,
.contentVisibilityto hide the OC self-entry — works (ocvalidateclean). - Tahoe recovery boots via OpenCore;
vm-driver.pydrives it entirely by screenshots + OCR: handles the OpenCore picker, opens Terminal (Ctrl-F2 → Utilities → Terminal), types the bootstrap command — works. - HFS+
VMIXvolume mounts in Recovery; the install script erases the disk as APFS "Macintosh HD", lays down the host-extractedInstall macOS Tahoe.appskeleton anddds a byte-exactSharedSupport.dmgfrom a raw disk mapped to that byte range of the pkg (verifiedsha256identical to Apple's) — works. startosinstallruns,osinstallersetupdmounts SharedSupport.dmg, reads the MobileAsset bundle (IA OS Version: 26.6.2, Build 25G83), loads the 641-product SU catalog from swscan.apple.com (so guest networking works), and logsMachine is VM, will assume APFS is supported.
Blocker 1 — OSISVerifyBaseSystemOperation: pkgdmg is missing a footer
After mounting the dmg, osinstallersetupd runs a verify step that treats
SharedSupport.dmg as a pkgdmg (Getting offset for dmg in pkg) and fails
pkgdmg is missing a footer → Installation cannot proceed because the installer is damaged (Code 255). But this InstallAssistant SharedSupport.dmg is a plain
UDIF image (first bytes eb 58 90 …, not xar!), and it is byte-identical to
Apple's — so this is not truncation or corruption (earlier truncation, from
Recovery's xar mishandling an 18 GB member, was fixed by the byte-range dd).
It is a macOS-internal verification that rejects the plain-UDIF SharedSupport
when running Tahoe's startosinstall in this recovery/VM. Network is fine (the
catalog loaded), so it is not the firewall case commonly cited for this error.
Leads not yet tried: driving the Tahoe recovery's network "Reinstall macOS" (GUI, downloads assets at install time — sidesteps the local-dmg verify); a different SMBIOS/board; or a newer OpenCore/kext combo. This is cutting-edge (Tahoe shipped 2025-09) and the hackintosh community is still working it out.
Blocker 2 — Tahoe recovery availability on Apple's CDN
osrecovery.apple.com is load-balancing latest across CDN nodes during the
Tahoe rollout: most requests return the Sequoia 15.4.1 BaseSystem
(082-33203), some return Tahoe 26.6.2 (140-93589). fetchRecovery
retries until it gets the pinned Tahoe hash, but that can exhaust its attempts
when Tahoe is rare. The robust fix is to self-host the verified Tahoe
BaseSystem.dmg (960530321 bytes, sha256 edddd0d5…, confirmed 26.6.2) the way
the Win10 ISO is hosted on git.sagar.ch, and point fetchRecovery at it.