macOS: drive the install and all customization from a Recovery "PE", no GUI

Replace the screenshot/OCR/keystroke driving of Apple's Recovery with a
"PE": BaseSystem.dmg (a journaled HFS+ volume, writable from Linux) with one
LaunchDaemon added (makeRecoveryPE) that runs /Volumes/VMIX/run.sh as root at
boot, records the status and powers off. launchd loads it alongside its signed
cache (verified on Tahoe 26.6.2); same idea as AutoNBI/Imagr NetBoot images.

- makeImage: the PE runs vmix-install.sh (erase, installer app, SharedSupport
  pkgdmg, startosinstall). Progress is read from the serial console
  (boot-args serial=3 -v, VMIX-* markers) and screenshots (brightness only).
  Fully offline; prepare now takes ~5 min instead of ~10.
- customizeImage: boots the PE with the image attached and runs the template
  offline against the mounted System/Data volumes; OpenCore ScanPolicy
  restricted to HFS+/SATA so only the PE can boot. One PE boot ~30 s. The
  installed macOS is never booted for customization, so nothing depends on
  launchd/BTM approval or a first-boot agent (removed).
- templates rewritten for offline use: generalize creates the user with
  dscl -f (admin, home, auto-login kcpassword, Setup Assistant suppression,
  hostname, locale, timezone, keyboard type, container resize); remote-access,
  no-updates, performance edit the target's plists.
- makeBootDisk: build-time OpenCore variant (serial console, ScanPolicy).
- vm-driver.py rewritten: passive observation only (serial markers, kernel
  boots, panics, brightness), disk+serial-aware hang watchdog, reboot-death
  reset, halt/loginwindow detection. No OCR/tesseract.
- OpenCore: four SMBIOS DIMMs for MacPro7,1 (no "Memory Modules
  Misconfigured" warning).
- tools/soak.sh: repeatability harness.

Verified on daku: base install 23 min end to end; basic + generalize in three
~30 s PE boots; the result auto-logs into the desktop with the created user.

Root cause of the "first-boot hang" (from the serial log): the guest's restart
path panics (IOPlatformHaltRestartAction -> AppleSMC, SMCWDT smcWriteKey
kSMCBadCommand, nested panic) because the pinned OSX-KVM Lilu disables itself
on macOS 26, so VirtualSMC never loads. Handled by the driver (reset within
60 s); kext update to follow.

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-09 11:21:24 -03:00
parent 58a317f5d2
commit 8dc8f4265d
24 changed files with 802 additions and 977 deletions

View file

@ -4,12 +4,10 @@
{
name = "no-updates";
script = ''
softwareupdate --schedule off || true
defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false
defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool false
defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -bool false
defaults write /Library/Preferences/com.apple.SoftwareUpdate ConfigDataInstall -bool false
defaults write /Library/Preferences/com.apple.SoftwareUpdate CriticalUpdateInstall -bool false
defaults write /Library/Preferences/com.apple.commerce AutoUpdate -bool false
SU="$DATA/Library/Preferences/com.apple.SoftwareUpdate.plist"
for k in AutomaticCheckEnabled AutomaticDownload AutomaticallyInstallMacOSUpdates ConfigDataInstall CriticalUpdateInstall; do
pe_plist_set "$SU" "$k" bool false
done
pe_plist_set "$DATA/Library/Preferences/com.apple.commerce.plist" AutoUpdate bool false
'';
}

View file

@ -1,11 +1,32 @@
# Less background work in a VM: no Spotlight indexing, no Time Machine, no sleep
{ ... }:
# Less background work in a VM: no Spotlight indexing, no Time Machine, no
# sleep, no immediate screen lock.
{ pkgs, ... }:
let
power = pkgs.writeText "com.apple.PowerManagement.plist" ''
<?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>ActivePowerProfiles</key><dict><key>AC Power</key><integer>-1</integer></dict>
<key>Custom Profile</key><dict><key>AC Power</key><dict>
<key>Display Sleep Timer</key><integer>0</integer>
<key>System Sleep Timer</key><integer>0</integer>
<key>Disk Sleep Timer</key><integer>0</integer>
<key>Wake On LAN</key><integer>0</integer>
<key>hibernatemode</key><integer>0</integer>
</dict></dict>
</dict>
</plist>
'';
in
{
name = "performance";
files = [ { source = power; name = "com.apple.PowerManagement.plist"; } ];
script = ''
mdutil -a -i off || true
tmutil disable || true
pmset -a sleep 0 displaysleep 0 disksleep 0 hibernatemode 0 womp 0 || true
defaults write /Library/Preferences/com.apple.loginwindow DisableScreenLockImmediate -bool true
touch "$DATA/.metadata_never_index"
pe_plist_set "$DATA/Library/Preferences/com.apple.TimeMachine.plist" AutoBackup bool false
pe_plist_set "$DATA/Library/Preferences/com.apple.loginwindow.plist" DisableScreenLockImmediate bool true
cp "$V/com.apple.PowerManagement.plist" "$DATA/Library/Preferences/com.apple.PowerManagement.plist"
chown 0:0 "$DATA/Library/Preferences/com.apple.PowerManagement.plist"
'';
}

View file

@ -1,11 +1,10 @@
# Enable SSH (Remote Login) and Screen Sharing (VNC on 5900 inside the guest)
# by clearing their launchd overrides on the image's Data volume.
{ ... }:
{
name = "remote-access";
script = ''
systemsetup -setremotelogin on >/dev/null 2>&1 || launchctl load -w /System/Library/LaunchDaemons/ssh.plist
launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist
# allow all local users to screen share
defaults write /var/db/launchd.db/com.apple.launchd/overrides.plist com.apple.screensharing -dict Disabled -bool false 2>/dev/null || true
pe_service_disabled com.apple.openssh.sshd false
pe_service_disabled com.apple.screensharing false
'';
}