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

@ -1,6 +1,11 @@
# Distribution-style flat package (xar + bom + cpio, built on Linux) for
# `startosinstall --installpackage`: installs the vmix agent LaunchDaemon and
# marks Setup Assistant as done so the first boot lands on loginwindow.
# `startosinstall --installpackage`. macOS installs it during the first boot of the
# installed system (bootinstalld, "Installer Progress"): it places the vmix agent
# LaunchDaemon, marks Setup Assistant as done, starts the agent, and schedules a
# reboot as a fallback so the daemon runs even if bootstrapping failed.
#
# The files are shipped inside Scripts and copied by postinstall: installd unpacks
# our Scripts archive fine, but "shoves 0 items" from a Linux-made Payload.
{ pkgs, lib, ... }:
{ version ? "1.0" }:
let
@ -9,36 +14,61 @@ let
bomutils = pkgs.bomutils.overrideAttrs (_: { hardeningDisable = [ "fortify" ]; });
postinstall = pkgs.writeText "postinstall" ''
#!/bin/sh
# $3 = target volume
T="$3"
mkdir -p "$T/private/var/db"
# Runs during the OS install (bootinstalld) with $3 = the target system root.
# Only place files; the ch.vmix.agent LaunchDaemon then runs on the installed
# system's first boot via RunAtLoad (confirmed loading on Tahoe).
T="''${3%/}"
HERE="$(cd "$(dirname "$0")" && pwd)"
LOG="$T/private/var/log/vmix-agent-install.log"
mkdir -p "$T/private/var/log"
exec >>"$LOG" 2>&1
echo "=== vmix agent pkg postinstall $(date) target=[$3] ==="
mkdir -p "$T/Library/LaunchDaemons" "$T/Library/vmix" "$T/private/var/db"
cp "$HERE/agent.sh" "$T/Library/vmix/agent.sh"
cp "$HERE/${id}.plist" "$T/Library/LaunchDaemons/${id}.plist"
chmod 755 "$T/Library/vmix/agent.sh"
chmod 644 "$T/Library/LaunchDaemons/${id}.plist"
chown -R root:wheel "$T/Library/vmix" "$T/Library/LaunchDaemons/${id}.plist"
touch "$T/private/var/db/.AppleSetupDone"
chown root:wheel "$T/private/var/db/.AppleSetupDone"
chmod 755 "$T/Library/vmix/agent.sh"
chown -R root:wheel "$T/Library/vmix" "$T/Library/LaunchDaemons/${id}.plist"
ls -la "$T/Library/vmix/agent.sh" "$T/Library/LaunchDaemons/${id}.plist"
# A pkg LaunchDaemon is registered with Background Task Management but stays
# pending approval, so it will not auto-run headless. Two BTM-exempt triggers:
# - bootstrap it now (starts it in the installer env; the agent no-ops there)
# - a root cron @reboot job (Apple's cron daemon is trusted, runs it at boot)
launchctl bootstrap system "$T/Library/LaunchDaemons/${id}.plist" 2>&1 && echo "bootstrapped" || echo "bootstrap returned $?"
mkdir -p "$T/usr/lib/cron/tabs"
printf '@reboot /bin/sh /Library/vmix/agent.sh\n' > "$T/usr/lib/cron/tabs/root"
chmod 600 "$T/usr/lib/cron/tabs/root"
chown root:wheel "$T/usr/lib/cron/tabs/root"
echo "cron @reboot installed"
exit 0
'';
in
pkgs.runCommand "vmix-agent-${version}.pkg" {
nativeBuildInputs = [ pkgs.xar bomutils pkgs.cpio pkgs.gzip ];
nativeBuildInputs = [ pkgs.xar bomutils pkgs.cpio pkgs.libarchive pkgs.gzip ];
} ''
mkdir -p root/Library/LaunchDaemons root/Library/vmix scripts flat/vmix-agent.pkg
cp ${../guest/agent.sh} root/Library/vmix/agent.sh
cp ${../guest/ch.vmix.agent.plist} root/Library/LaunchDaemons/${id}.plist
chmod 755 root/Library/vmix/agent.sh
chmod 644 root/Library/LaunchDaemons/${id}.plist
# the same files ride along in Scripts, which is what postinstall installs from
cp ${postinstall} scripts/postinstall
chmod 755 scripts/postinstall
cp ${../guest/agent.sh} scripts/agent.sh
cp ${../guest/ch.vmix.agent.plist} scripts/${id}.plist
chmod 755 scripts/postinstall scripts/agent.sh
NFILES=$(find root | wc -l)
KBYTES=$(du -sk root | cut -f1)
(cd root && find . | cpio -o --format odc --owner 0:0 --quiet | gzip -c > ../flat/vmix-agent.pkg/Payload)
# bsdcpio keeps the "./" prefix the Bom uses (GNU cpio strips it and installd then extracts nothing)
(cd root && find . | bsdcpio -o --format odc --quiet | gzip -c > ../flat/vmix-agent.pkg/Payload)
(cd scripts && find . | cpio -o --format odc --owner 0:0 --quiet | gzip -c > ../flat/vmix-agent.pkg/Scripts)
mkbom -u 0 -g 80 root flat/vmix-agent.pkg/Bom
cat > flat/vmix-agent.pkg/PackageInfo <<XML
<?xml version="1.0" encoding="utf-8"?>
<pkg-info overwrite-permissions="true" relocatable="false" identifier="${id}" postinstall-action="none" version="${version}" format-version="2" generated-by="vmix" auth="root">
<pkg-info overwrite-permissions="true" relocatable="false" identifier="${id}" postinstall-action="none" version="${version}" format-version="2" generated-by="vmix" auth="root" install-location="/">
<payload installKBytes="$KBYTES" numberOfFiles="$NFILES"/>
<bundle-version/>
<upgrade-bundle/>