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:
Git Sagar 2026-09-08 16:03:52 -03:00
parent 6a62a649bd
commit 242e48a5fc
35 changed files with 1842 additions and 36 deletions

View file

@ -0,0 +1,30 @@
#!/bin/sh
# vmix agent: LaunchDaemon that runs at every boot as root. If a volume named
# VMIX carrying vmix-run.sh is attached, run it, record the result on the
# volume and power off. Without the volume it is a no-op (normal boot).
# This is the macOS counterpart of the Windows Audit Mode RunOnce script.
LOG=/var/log/vmix-agent.log
exec >>"$LOG" 2>&1
echo "=== vmix agent: $(date) ==="
V=/Volumes/VMIX
i=0
while [ ! -f "$V/vmix-run.sh" ] && [ $i -lt 60 ]; do
diskutil mount VMIX >/dev/null 2>&1
sleep 2
i=$((i + 1))
done
if [ ! -f "$V/vmix-run.sh" ]; then
echo "vmix agent: no VMIX volume, normal boot"
exit 0
fi
echo "vmix agent: running vmix-run.sh"
cd "$V" || exit 1
sh "$V/vmix-run.sh" >"$V/vmix-run.log" 2>&1
rc=$?
echo "vmix agent: vmix-run.sh exited $rc"
echo "$rc" >"$V/vmix-run.status"
cp "$LOG" "$V/vmix-agent.log" 2>/dev/null
sync
sleep 2
diskutil unmount force "$V" >/dev/null 2>&1
shutdown -h now

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ch.vmix.agent</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>/Library/vmix/agent.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/vmix-agent.log</string>
<key>StandardErrorPath</key>
<string>/var/log/vmix-agent.log</string>
</dict>
</plist>

View file

@ -0,0 +1,11 @@
#!/usr/bin/env python3
# Encode a password as /etc/kcpassword (macOS auto-login). XOR with Apple's
# fixed key, zero padded to a multiple of 12 so a key byte terminates it.
import sys
KEY = [0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F]
pw = list(sys.argv[1].encode()) if len(sys.argv) > 1 else []
pw += [0] * (12 - len(pw) % 12)
out = bytes(b ^ KEY[i % len(KEY)] for i, b in enumerate(pw))
sys.stdout.buffer.write(out)

View file

@ -0,0 +1,117 @@
#!/bin/sh
# vmix: automated macOS install. Runs inside macOS Recovery's Terminal, started
# by vm-driver.py which types "sh /Volumes/VMIX/run.sh" for us.
#
# 1. erase the target disk (found by size) as APFS "Macintosh HD"
# 2. rebuild "Install macOS <name>.app": app skeleton from installer-app.tar
# (host-extracted Payload) + SharedSupport.dmg copied byte-exact by dd from a
# raw disk that maps that byte range of the pkg (Recovery's xar truncates an
# 18 GB member, which makes startosinstall report "pkgdmg is missing a footer")
# 3. startosinstall unattended, with the vmix agent pkg as --installpackage
# 4. when the prepare phase is done (SIGUSR1) also drop the agent + .AppleSetupDone
# onto the volume, then let the installer reboot
#
# On first boot of the installed system the agent runs /Volumes/VMIX/vmix-run.sh
# and powers off, which ends the QEMU session on the host.
# macOS Recovery invokes us as `sh` (bash in POSIX mode, no process substitution);
# re-exec once under bash so `>(tee ...)` and other bashisms work.
if [ -z "${VMIX_REEXEC:-}" ]; then VMIX_REEXEC=1 exec bash "$0" "$@"; fi
V="/Volumes/VMIX"
# tee to the Terminal (visible in host screenshots) and to a log on the volume
exec > >(tee "$V/install.log") 2>&1
set -x
. "$V/vmix.conf"
fail() {
echo "vmix-install: FAIL: $*"
cp /var/log/install.log "$V/system-install.log" 2>/dev/null || true
echo 1 >"$V/install.status"
sync
sleep 2
shutdown -h now 2>/dev/null || halt 2>/dev/null || true
exit 1
}
# whole-disk identifier (diskN) whose size in bytes is exactly $1
disk_by_size() {
for d in $(diskutil list | grep -oE '^/dev/disk[0-9]+'); do
s=$(diskutil info "$d" | sed -n 's/.*Disk Size:.*(\([0-9][0-9]*\) Bytes).*/\1/p')
[ "$s" = "$1" ] && { echo "${d#/dev/}"; return 0; }
done
return 1
}
echo "vmix-install: $(date) app=$APP_NAME volume=$VOLUME_NAME"
TARGET=$(disk_by_size "$TARGET_BYTES") || fail "target disk ($TARGET_BYTES bytes) not found"
SSDISK=$(disk_by_size "$SS_DISK_BYTES") || fail "SharedSupport disk ($SS_DISK_BYTES bytes) not found"
echo "vmix-install: target=$TARGET sharedsupport=$SSDISK"
# --- 1. erase the target disk as an APFS volume
diskutil eraseDisk APFS "$VOLUME_NAME" GPT "$TARGET" || fail "eraseDisk $TARGET"
VOL="/Volumes/$VOLUME_NAME"
[ -d "$VOL" ] || fail "$VOL not mounted"
# --- 2. rebuild the installer app on the target volume
tar -xf "$V/installer-app.tar" -C "$VOL" || fail "untar installer-app.tar"
APP="$VOL/$APP_NAME"
SOI="$APP/Contents/Resources/startosinstall"
[ -x "$SOI" ] || fail "startosinstall missing in $APP"
SS="$APP/Contents/SharedSupport/SharedSupport.dmg"
mkdir -p "$APP/Contents/SharedSupport"
FULL=$((SS_LEN / 1048576))
REM=$((SS_LEN % 1048576))
dd if="/dev/r$SSDISK" of="$SS" bs=1048576 count=$FULL || fail "dd SharedSupport.dmg"
if [ "$REM" -gt 0 ]; then
dd if="/dev/r$SSDISK" bs=1048576 skip=$FULL count=1 2>/dev/null | dd bs=1 count=$REM >>"$SS" || fail "dd SharedSupport.dmg tail"
fi
[ "$(stat -f %z "$SS")" = "$SS_LEN" ] || fail "SharedSupport.dmg size mismatch: $(stat -f %z "$SS") != $SS_LEN"
tail -c 512 "$SS" | grep -qa koly || fail "SharedSupport.dmg has no UDIF koly footer"
chflags -h norestricted "$SS" 2>/dev/null || true
echo "vmix-install: app=$APP SharedSupport.dmg=$(stat -f %z "$SS") bytes"
# macOS certificate validation needs a sane clock; a fresh VM RTC can be wrong.
echo "vmix-install: guest clock is $(date) (UTC $(date -u))"
if [ -n "${BUILD_DATE:-}" ]; then
date -u "$BUILD_DATE" && echo "vmix-install: set clock to $(date)"
fi
# --- 3. unattended install
PREPARED=0
trap 'PREPARED=1' USR1
run_install() {
"$SOI" --volume "$VOL" --agreetolicense --nointeraction --pidtosignal $$ "$@" &
INSTALL_PID=$!
while :; do
wait $INSTALL_PID
rc=$?
[ "$PREPARED" = 1 ] && return 0
kill -0 $INSTALL_PID 2>/dev/null || return $rc
done
}
run_install --rebootdelay 300 --installpackage "$V/vmix-agent.pkg" \
|| { echo "vmix-install: retry without --rebootdelay"; run_install --installpackage "$V/vmix-agent.pkg"; } \
|| { echo "vmix-install: retry without --installpackage"; run_install; } \
|| fail "startosinstall"
# --- 4. prepare phase done: also drop the agent onto the volume directly.
T="$VOL"
if [ -d "$T" ]; then
mkdir -p "$T/Library/LaunchDaemons" "$T/Library/vmix" "$T/private/var/db"
cp "$V/agent/agent.sh" "$T/Library/vmix/agent.sh"
cp "$V/agent/ch.vmix.agent.plist" "$T/Library/LaunchDaemons/ch.vmix.agent.plist"
chmod 755 "$T/Library/vmix/agent.sh"
chmod 644 "$T/Library/LaunchDaemons/ch.vmix.agent.plist"
chown -R root:wheel "$T/Library/vmix" "$T/Library/LaunchDaemons/ch.vmix.agent.plist"
touch "$T/private/var/db/.AppleSetupDone"
chown root:wheel "$T/private/var/db/.AppleSetupDone"
else
echo "vmix-install: WARNING: $T not mounted after prepare, relying on --installpackage"
fi
echo 0 >"$V/install.status"
sync
kill -USR1 $INSTALL_PID
wait $INSTALL_PID
exit 0