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:
parent
58a317f5d2
commit
8dc8f4265d
24 changed files with 802 additions and 977 deletions
|
|
@ -5,7 +5,7 @@ rec {
|
|||
essentials = {
|
||||
remoteAccess = import ./essentials/remote-access.nix { };
|
||||
noUpdates = import ./essentials/no-updates.nix { };
|
||||
performance = import ./essentials/performance.nix { };
|
||||
performance = import ./essentials/performance.nix { inherit pkgs; };
|
||||
};
|
||||
|
||||
bundles = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# Generalize a macOS image: create the user, auto-login, hostname, timezone,
|
||||
# suppress Setup Assistant prompts, then remove the vmix agent. Also gives 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.
|
||||
# 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, ... }:
|
||||
|
|
@ -34,7 +35,8 @@ let
|
|||
"DidSeeCloudSetup" "DidSeeSiriSetup" "DidSeePrivacy" "DidSeeTouchIDSetup" "DidSeeAppearanceSetup"
|
||||
"DidSeeScreenTime" "DidSeeAccessibility" "DidSeeTrueTonePrivacy" "DidSeeActivationLock"
|
||||
"DidSeeiCloudLoginForStorageServices" "DidSeeSyncSetup" "DidSeeSyncSetup2" "DidSeeAppleIDSyncSetup"
|
||||
"DidSeeApplePaySetup" "DidSeeIntelligence" "DidSeeLockdownMode" "DidSeeAppStore" "SkipFirstLoginOptimization"
|
||||
"DidSeeApplePaySetup" "DidSeeIntelligence" "DidSeeLockdownMode" "DidSeeAppStore" "DidSeeUpdateMacAutomatically"
|
||||
"DidSeeSoftwareUpdate" "SkipFirstLoginOptimization"
|
||||
];
|
||||
in
|
||||
{
|
||||
|
|
@ -44,60 +46,81 @@ in
|
|||
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)
|
||||
if ! id "${username}" >/dev/null 2>&1; then
|
||||
sysadminctl -addUser "${username}" -fullName ${lib.escapeShellArg fullName} \
|
||||
-password ${lib.escapeShellArg (if password == "" then tempPassword else password)} \
|
||||
-admin -home "/Users/${username}" || exit 1
|
||||
${lib.optionalString (password == "") ''
|
||||
dscl . -passwd "/Users/${username}" "${tempPassword}" "" || echo "vmix: WARNING: could not set an empty password, password is '${tempPassword}'"
|
||||
''}
|
||||
# --- 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 ''
|
||||
defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "${username}"
|
||||
cp /Volumes/VMIX/kcpassword /etc/kcpassword
|
||||
chmod 600 /etc/kcpassword
|
||||
chown root:wheel /etc/kcpassword
|
||||
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
|
||||
P="/Users/${username}/Library/Preferences/com.apple.SetupAssistant"
|
||||
VER=$(sw_vers -productVersion)
|
||||
BUILD=$(sw_vers -buildVersion)
|
||||
for k in ${lib.concatStringsSep " " setupKeys}; do
|
||||
defaults write "$P" "$k" -bool true
|
||||
done
|
||||
defaults write "$P" GestureMovieSeen none
|
||||
defaults write "$P" LastSeenCloudProductVersion "$VER"
|
||||
defaults write "$P" LastSeenBuddyBuildVersion "$BUILD"
|
||||
defaults write "$P" LastSeenSiriProductVersion "$VER"
|
||||
defaults write "$P" LastPreLoginTasksPerformedVersion "$VER"
|
||||
defaults write "/Users/${username}/Library/Preferences/.GlobalPreferences" AppleLocale "${macLocale}"
|
||||
chown -R "${username}" "/Users/${username}/Library/Preferences"
|
||||
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
|
||||
scutil --set ComputerName "${hostname}"
|
||||
scutil --set HostName "${hostname}"
|
||||
scutil --set LocalHostName "${hostname}"
|
||||
defaults write /Library/Preferences/.GlobalPreferences AppleLocale "${macLocale}"
|
||||
systemsetup -settimezone "${timezone}" >/dev/null 2>&1 || ln -sfn "/usr/share/zoneinfo/${timezone}" /etc/localtime
|
||||
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
|
||||
|
||||
# --- never sleep (VM)
|
||||
pmset -a sleep 0 displaysleep 0 disksleep 0 hibernatemode 0 || true
|
||||
# --- 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 / | awk '/APFS Physical Store/ {print $NF}')
|
||||
STORE=$(diskutil info "$SYS_ID" | sed -n 's/.*APFS Physical Store: *//p' | awk '{print $1}')
|
||||
[ -n "$STORE" ] && diskutil apfs resizeContainer "$STORE" 0 || true
|
||||
|
||||
${lib.optionalString delayOobeRun ''
|
||||
# Setup Assistant will run on the next boot
|
||||
rm -f /var/db/.AppleSetupDone
|
||||
''}
|
||||
|
||||
# --- the agent's job is done: remove it (this is the last vmix step)
|
||||
rm -f /Library/LaunchDaemons/ch.vmix.agent.plist
|
||||
rm -rf /Library/vmix
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue