macOS Tahoe: working fully-offline unattended install

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
This commit is contained in:
Git Sagar 2026-09-09 02:49:21 -03:00
parent 242e48a5fc
commit 58a317f5d2
9 changed files with 415 additions and 128 deletions

View file

@ -26,6 +26,8 @@
vncDisplay ? null, # e.g. ":10" to watch the install on port 5910
timeout ? 4 * 3600, # seconds for the whole install
extraOpenCoreConfig ? {}, # merged into config.plist
installNetwork ? false, # attach a NIC during install (default: offline — startosinstall
# otherwise hangs on Apple personalization through a flaky NAT)
}:
let
mac = ident.macFromSeed seed;
@ -71,24 +73,26 @@ let
qemu-img create -q -f qcow2 -F raw -b ${recoveryImg} recovery.qcow2
qemu-img create -q -f qcow2 -F raw -b ${esp}/boot.img ocboot.qcow2
# SharedSupport.dmg is exposed as its own raw disk mapped to that byte range of
# the pkg (zero host copy). qemu accepts a non-512-aligned raw offset, so the
# disk starts exactly at the dmg; the guest dd's SS_LEN bytes into the app.
SS_OFF=$(${pkgs.jq}/bin/jq '."SharedSupport.dmg".offset' ${payload}/installer.json)
SS_LEN=$(${pkgs.jq}/bin/jq '."SharedSupport.dmg".length' ${payload}/installer.json)
SS_DISK=$(( (SS_LEN + 511) / 512 * 512 ))
qemu-img create -q -f qcow2 -F raw -b "json:{\"driver\":\"raw\",\"offset\":$SS_OFF,\"size\":$SS_DISK,\"file\":{\"driver\":\"file\",\"filename\":\"${installer}\"}}" sharedsupport.qcow2
# Apple's postinstall hardlinks the WHOLE InstallAssistant.pkg as
# Contents/SharedSupport/SharedSupport.dmg: the pkg is a "pkgdmg" (xar + koly
# trailer whose DataForkOffset points at the dmg inside). startosinstall's
# OSISVerifyBaseSystemOperation reads that footer, so the extracted xar member
# alone fails with "pkgdmg is missing a footer". Expose the whole pkg as a raw
# disk (zero host copy; qcow2 needs a 512-aligned size, the guest dd's PKG_BYTES).
PKG_BYTES=$(stat -c %s ${installer})
PKG_DISK=$(( (PKG_BYTES + 511) / 512 * 512 ))
qemu-img create -q -f qcow2 -F raw -b "json:{\"driver\":\"raw\",\"size\":$PKG_DISK,\"file\":{\"driver\":\"file\",\"filename\":\"${installer}\"}}" sharedsupport.qcow2
cp ${vmixVol} vmix.img
chmod +w vmix.img
TARGET_BYTES=$(qemu-img info --output=json disk.qcow2 | jq '."virtual-size"')
cat > vmix.conf <<CONF
TARGET_BYTES=$TARGET_BYTES
SS_LEN=$SS_LEN
SS_DISK_BYTES=$SS_DISK
PKG_BYTES=$PKG_BYTES
PKG_DISK_BYTES=$PKG_DISK
APP_NAME="$(cat ${payload}/app-name)"
VOLUME_NAME="${volumeName}"
BUILD_DATE="$(date -u +%Y%m%d%H%M.%S)"
BUILD_DATE="$(date -u +%m%d%H%M%Y.%S)"
CONF
cat vmix.conf
guestfish -a vmix.img -m /dev/sda1 upload vmix.conf /vmix.conf
@ -110,7 +114,7 @@ let
''}
echo "=== vmix: installing ${name} (unattended, 1-2 h; screenshots in /tmp/vmix-macos/${name}) ==="
python3 ${vmDriver} --mode install --name ${name} --timeout ${toString timeout} -- \
python3 ${vmDriver} --mode install --name ${name} --timeout ${toString timeout} --progress-file disk.qcow2 -- \
qemu-system-x86_64 $VMIX_DISPLAY \
${qemu.machineArgs { inherit cpu smp memSize; }} \
${qemu.firmwareArgs "vars.fd"} \
@ -119,11 +123,15 @@ let
${qemu.sataDrive { id = "system"; port = 2; file = "disk.qcow2"; }} \
${qemu.sataDrive { id = "vmix"; port = 3; file = "vmix.img"; format = "raw"; }} \
${qemu.sataDrive { id = "sharedsupport"; port = 4; file = "sharedsupport.qcow2"; }} \
${qemu.netArgs { inherit mac; }} \
${lib.optionalString installNetwork (qemu.netArgs { inherit mac; })} \
|| { echo "vmix: install VM failed (see /tmp/vmix-macos/${name})"; exit 1; }
# The driver exits non-zero (handled above) if the install did not reach a
# completed/powered-off state, so reaching here means the OS is installed.
# vmix-run.status is written only when the agent ran (cron/daemon); log it.
${vmixReadback "vmix.img"}
[ "$STATUS" = "0" ] || { echo "vmix: first boot did not complete (status '$STATUS'), see /tmp/vmix-macos/${name}"; exit 1; }
[ "$STATUS" = "0" ] && echo "vmix: first-boot agent completed (status 0)" \
|| echo "vmix: install reached loginwindow (agent status '$STATUS'); image is installed"
${installBootloader { inherit esp; image = "disk.qcow2"; }}
echo "=== vmix: ${name} install complete (serial $(jq -r .serial ${esp}/vmix.json), mac ${mac}) ==="