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
134
lib/images/macos/README.md
Normal file
134
lib/images/macos/README.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# 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.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-<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.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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue