# 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 --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.enable` override the defaults. * `vmix copy` writes the image to a disk but cannot grow APFS from Linux (`diskutil apfs resizeContainer disk0s2 0` in macOS afterwards). ## Debugging a build Screenshots (`NNN-.png`), `driver.log` and the QMP socket of every VM session are in `/tmp/vmix-macos//` 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 `; 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.qcow2` at 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.py` adjusted. * Hosts must run KVM with an AVX2-capable CPU (Intel or AMD; the guest sees a Skylake). `sandbox = relaxed` and the `kvm` system feature, as for Windows. * Software updates inside the VM are disabled by the `noUpdates` template (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, `.contentVisibility` to hide the OC self-entry — **works** (`ocvalidate` clean). * Tahoe recovery boots via OpenCore; `vm-driver.py` drives it entirely by screenshots + OCR: handles the OpenCore picker, opens Terminal (Ctrl-F2 → Utilities → Terminal), types the bootstrap command — **works**. * HFS+ `VMIX` volume mounts in Recovery; the install script erases the disk as APFS "Macintosh HD", lays down the host-extracted `Install macOS Tahoe.app` skeleton and `dd`s a **byte-exact** `SharedSupport.dmg` from a raw disk mapped to that byte range of the pkg (verified `sha256` identical to Apple's) — **works**. * `startosinstall` runs, `osinstallersetupd` **mounts 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 logs `Machine 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.