vmix.nix/lib/images/macos/templates/generalize.nix
Git Sagar 8dc8f4265d 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
2026-09-10 13:33:38 -03:00

126 lines
6.4 KiB
Nix

# Generalize a macOS image, offline from the PE: create the (admin) user on the
# image's Data volume with dscl, auto-login, suppress the first-login Setup
# Assistant, hostname, locale, timezone, use the whole disk, and give the image
# a fresh SMBIOS identity (serial/MLB from macserial, MAC + UUID from `seed`) so
# every generalized VM looks like a distinct Mac to Apple ID/iMessage.
# Usage: (templates.generalize { username = "User"; password = ""; hostname = "MAC"; })
# delayOobeRun = true: no user, Setup Assistant runs on first real boot (like Windows OOBE)
{ pkgs, lib, ... }:
{
username ? "User",
password ? "",
fullName ? username,
autoLogon ? true,
hostname ? "MAC-VM",
locale ? "en-US",
timezone ? "UTC",
delayOobeRun ? false,
# SMBIOS identity; anything unset is generated
model ? null,
serial ? null,
mlb ? null,
uuid ? null,
mac ? null,
seed ? "${hostname}-${username}",
# accepted for CLI parity with Windows, not supported on macOS
bgColor ? null,
}:
let
kcpasswordFile = pkgs.runCommand "kcpassword" { nativeBuildInputs = [ pkgs.python3 ]; } ''
python3 ${../guest/kcpassword.py} ${lib.escapeShellArg password} > $out
'';
macLocale = builtins.replaceStrings [ "-" ] [ "_" ] locale;
tempPassword = "vmix-temp-password";
setupKeys = [
"DidSeeCloudSetup" "DidSeeSiriSetup" "DidSeePrivacy" "DidSeeTouchIDSetup" "DidSeeAppearanceSetup"
"DidSeeScreenTime" "DidSeeAccessibility" "DidSeeTrueTonePrivacy" "DidSeeActivationLock"
"DidSeeiCloudLoginForStorageServices" "DidSeeSyncSetup" "DidSeeSyncSetup2" "DidSeeAppleIDSyncSetup"
"DidSeeApplePaySetup" "DidSeeIntelligence" "DidSeeLockdownMode" "DidSeeAppStore" "DidSeeUpdateMacAutomatically"
"DidSeeSoftwareUpdate" "SkipFirstLoginOptimization"
];
in
{
name = if delayOobeRun then "generalize-delay-oobe" else "generalize";
files = [ { source = kcpasswordFile; name = "kcpassword"; } ];
smbios = { inherit seed; } // lib.filterAttrs (_: v: v != null) { inherit model serial mlb uuid mac; };
script = ''
set -x
${lib.optionalString (bgColor != null) ''echo "vmix: bgColor is not supported on macOS, ignoring"''}
VER=$(pe_target_version); BUILD=$(pe_target_build)
echo "vmix: target macOS $VER ($BUILD)"
N="$DATA/private/var/db/dslocal/nodes/Default"
D() { dscl -f "$N" localhost "$@"; }
${lib.optionalString (!delayOobeRun) ''
# --- user account (admin), created directly in the local directory node
U="${username}"; HOME_DIR="$DATA/Users/$U"
if ! D -read "/Local/Default/Users/$U" >/dev/null 2>&1; then
UID_NEW=$(D -list /Local/Default/Users UniqueID | awk '$2 >= 501 && $2 < 1000 && $2 > m {m = $2} END {print (m ? m + 1 : 501)}')
D -create "/Local/Default/Users/$U" || pe_fail "dscl create user"
D -create "/Local/Default/Users/$U" UserShell /bin/zsh
D -create "/Local/Default/Users/$U" RealName ${lib.escapeShellArg fullName}
D -create "/Local/Default/Users/$U" UniqueID "$UID_NEW"
D -create "/Local/Default/Users/$U" PrimaryGroupID 20
D -create "/Local/Default/Users/$U" NFSHomeDirectory "/Users/$U"
if ! D -passwd "/Local/Default/Users/$U" ${lib.escapeShellArg password}; then
echo "vmix: WARNING: could not set the requested password, using '${tempPassword}'"
D -passwd "/Local/Default/Users/$U" "${tempPassword}" || pe_fail "dscl passwd"
fi
for g in admin _appserverusr _appserveradm _lpadmin; do
D -append "/Local/Default/Groups/$g" GroupMembership "$U" 2>/dev/null || true
done
mkdir -p "$HOME_DIR"
T="$SYS/System/Library/User Template/Non_localized"; [ -d "$T" ] || T="/System/Library/User Template/Non_localized"
ditto "$T" "$HOME_DIR" 2>/dev/null || true
L="$SYS/System/Library/User Template/English.lproj"; [ -d "$L" ] && ditto "$L" "$HOME_DIR" 2>/dev/null || true
else
UID_NEW=$(D -read "/Local/Default/Users/$U" UniqueID | awk '{print $2}')
fi
${lib.optionalString autoLogon ''
pe_plist_set "$DATA/Library/Preferences/com.apple.loginwindow.plist" autoLoginUser string "$U"
cp "$V/kcpassword" "$DATA/private/etc/kcpassword"
chmod 600 "$DATA/private/etc/kcpassword"; chown 0:0 "$DATA/private/etc/kcpassword"
''}
# --- no Setup Assistant / "What's new" prompts at first login
mkdir -p "$HOME_DIR/Library/Preferences"
P="$HOME_DIR/Library/Preferences/com.apple.SetupAssistant.plist"
for k in ${lib.concatStringsSep " " setupKeys}; do pe_plist_set "$P" "$k" bool true; done
pe_plist_set "$P" GestureMovieSeen string none
pe_plist_set "$P" LastSeenCloudProductVersion string "$VER"
pe_plist_set "$P" LastSeenBuddyBuildVersion string "$BUILD"
pe_plist_set "$P" LastSeenSiriProductVersion string "$VER"
pe_plist_set "$P" LastPreLoginTasksPerformedVersion string "$VER"
pe_plist_set "$P" LastPreLoginTasksPerformedBuild string "$BUILD"
pe_plist_set "$HOME_DIR/Library/Preferences/.GlobalPreferences.plist" AppleLocale string "${macLocale}"
chown -R "$UID_NEW:20" "$HOME_DIR"
touch "$DATA/private/var/db/.AppleSetupDone"
''}
${lib.optionalString delayOobeRun ''
rm -f "$DATA/private/var/db/.AppleSetupDone"
''}
# --- machine identity
PF="$DATA/Library/Preferences/SystemConfiguration/preferences.plist"
mkdir -p "$(dirname "$PF")"
pe_plist_dict "$PF" System
pe_plist_dict "$PF" System.System
pe_plist_dict "$PF" System.Network
pe_plist_dict "$PF" System.Network.HostNames
pe_plist_set "$PF" System.System.ComputerName string "${hostname}"
pe_plist_set "$PF" System.System.HostName string "${hostname}"
pe_plist_set "$PF" System.Network.HostNames.LocalHostName string "${hostname}"
pe_plist_set "$DATA/Library/Preferences/.GlobalPreferences.plist" AppleLocale string "${macLocale}"
ln -sfn "/var/db/timezone/zoneinfo/${timezone}" "$DATA/private/etc/localtime"
pe_plist_set "$DATA/Library/Preferences/com.apple.timezone.auto.plist" Active bool false
# --- QEMU's USB keyboard (vendor 0x0627, product 0x0001) is unknown to macOS,
# which would open the Keyboard Setup Assistant at every login: declare it ANSI
KT="$DATA/Library/Preferences/com.apple.keyboardtype.plist"
pe_plist_dict "$KT" keyboardtype
pe_plist_set "$KT" keyboardtype.1-1575-0 integer 40
# --- use the whole (possibly grown) disk
STORE=$(diskutil info "$SYS_ID" | sed -n 's/.*APFS Physical Store: *//p' | awk '{print $1}')
[ -n "$STORE" ] && diskutil apfs resizeContainer "$STORE" 0 || true
'';
}