#!/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 .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