vmix.nix/lib/images/macos/guest/vmix-install.sh
Git Sagar 58a317f5d2 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
2026-09-10 13:33:38 -03:00

139 lines
6.7 KiB
Bash

#!/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 = the WHOLE InstallAssistant.pkg
# dd'd byte-exact from a raw disk (Apple's own postinstall hardlinks the pkg
# there: it is a "pkgdmg" whose koly footer points at the dmg inside; the bare
# xar member fails startosinstall with "pkgdmg is missing a footer")
# 3. startosinstall unattended, with the vmix agent pkg as --installpackage
# 4. startosinstall reboots itself into the install phase; the vmix agent pkg
# installs during that phase and runs on the installed system's first boot
#
# 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"
# keep the recovery display awake so the host driver can watch the screen
caffeinate -dimsu -t 86400 >/dev/null 2>&1 &
pmset -a displaysleep 0 sleep 0 >/dev/null 2>&1 || true
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 "$PKG_DISK_BYTES") || fail "installer pkg disk ($PKG_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=$((PKG_BYTES / 1048576))
REM=$((PKG_BYTES % 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")" = "$PKG_BYTES" ] || fail "SharedSupport.dmg size mismatch: $(stat -f %z "$SS") != $PKG_BYTES"
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
# Blackhole Apple's install/verify endpoints so osinstallersetupd's network calls
# fail immediately instead of timing out (prepare otherwise crawls). Fully offline.
for d in swscan.apple.com swcdn.apple.com swdist.apple.com swquery.apple.com \
gs.apple.com gsa.apple.com gdmf.apple.com mesu.apple.com xp.apple.com \
albert.apple.com captive.apple.com deviceservices-external.apple.com \
identity.apple.com ppq.apple.com crl.apple.com ocsp.apple.com \
ocsp2.apple.com valid.apple.com; do
echo "127.0.0.1 $d" >> /etc/hosts
done
echo "vmix-install: blackholed Apple install endpoints for a fast offline prepare"
# --- 3. unattended install. startosinstall prepares then reboots the machine
# itself into the install phase. Prepare intermittently stalls (~46% — an online
# verify/personalization step through the VM's NAT), so a watchdog kills and
# retries startosinstall if the target volume makes no write progress for a while.
# The vmix agent pkg installs during the install phase and runs on first boot.
# quote args properly — $VOL contains a space ("Macintosh HD")
run_soi() { "$SOI" --volume "$VOL" --agreetolicense --nointeraction --rebootdelay 5 "$@"; }
free_kb() { df -k "$VOL" 2>/dev/null | awk 'NR==2 {print $4}'; }
attempt=0
while [ "$attempt" -lt 10 ]; do
attempt=$((attempt + 1))
echo "vmix-install: startosinstall attempt $attempt"
if [ "$attempt" -eq 1 ]; then
run_soi --installpackage "$V/vmix-agent.pkg" 2>&1 &
else
# a stalled attempt leaves the volume dirty; re-erase and rebuild for a clean retry
diskutil eraseDisk APFS "$VOLUME_NAME" GPT "$TARGET" || fail "eraseDisk on retry"
tar -xf "$V/installer-app.tar" -C "$VOL" || fail "untar on retry"
mkdir -p "$APP/Contents/SharedSupport"
dd if="/dev/r$SSDISK" of="$SS" bs=1048576 count=$FULL 2>/dev/null
[ "$REM" -gt 0 ] && dd if="/dev/r$SSDISK" bs=1048576 skip=$FULL count=1 2>/dev/null | dd bs=1 count=$REM >>"$SS" 2>/dev/null
chflags -h norestricted "$SS" 2>/dev/null || true
run_soi --installpackage "$V/vmix-agent.pkg" 2>&1 &
fi
SOI_PID=$!
# watchdog: kill startosinstall if free space stalls for ~4 min OR the attempt
# simply takes too long (prepare is intermittently slow; healthy = a few minutes)
last=$(free_kb); stalled=0; elapsed=0
while kill -0 "$SOI_PID" 2>/dev/null; do
sleep 30; elapsed=$((elapsed + 30))
now=$(free_kb)
if [ "$now" = "$last" ]; then stalled=$((stalled + 30)); else stalled=0; last=$now; fi
if [ "$stalled" -ge 240 ] || [ "$elapsed" -ge 540 ]; then
echo "vmix-install: prepare too slow (stalled=${stalled}s elapsed=${elapsed}s), killing to retry"
kill -9 "$SOI_PID" 2>/dev/null; pkill -9 -f startosinstall 2>/dev/null
break
fi
done
wait "$SOI_PID" 2>/dev/null
# on success startosinstall reboots the machine and we never get here
echo "vmix-install: startosinstall attempt $attempt ended without rebooting"
sleep 3
done
fail "startosinstall did not complete after $attempt attempts"