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
|
|
@ -1,13 +1,16 @@
|
|||
# Customize a macOS image by booting it with a VMIX volume: the vmix agent
|
||||
# (LaunchDaemon installed by makeImage) runs `script` as root, records the exit
|
||||
# status on the volume and powers off. Optionally re-installs OpenCore with a new
|
||||
# SMBIOS identity (`smbios`). Counterpart of the Windows auditScript flow.
|
||||
# Customize a macOS image offline from the vmix PE: the recovery boots with the
|
||||
# image and a VMIX volume attached, its hook runs `script` as root with the
|
||||
# image's System (read-only) and Data (rw) volumes mounted at $SYS / $DATA, then
|
||||
# powers off. The installed macOS itself is never booted, so nothing depends on
|
||||
# launchd/BTM approval inside the guest. Counterpart of the Windows
|
||||
# registry/audit flow. Optionally re-installs OpenCore with a new SMBIOS
|
||||
# identity (`smbios`).
|
||||
#
|
||||
# Templates provide:
|
||||
# script — sh script run as root on the booted system
|
||||
# script — sh script run as root in the PE (pe-lib.sh helpers available)
|
||||
# files — [{ source; name; }] extra files placed next to it on /Volumes/VMIX
|
||||
# smbios — { model? serial? mlb? uuid? mac? seed? } → fresh OpenCore config in the ESP
|
||||
{ pkgs, lib, qemu, ident, makeVmixVolume, makeOpenCore, installBootloader, vmixReadback, vmDriver, ... }:
|
||||
{ pkgs, lib, qemu, ident, makeVmixVolume, makeOpenCore, makeBootDisk, installBootloader, vmixReadback, vmDriver, ... }:
|
||||
originalImage: {
|
||||
name ? "",
|
||||
script ? "",
|
||||
|
|
@ -19,7 +22,7 @@ originalImage: {
|
|||
smp ? 4,
|
||||
memSize ? 4096,
|
||||
cpu ? qemu.defaultCpu,
|
||||
timeout ? 3600,
|
||||
timeout ? 1800,
|
||||
}:
|
||||
let
|
||||
originalImageName = lib.strings.removeSuffix "-vmix" (lib.strings.removeSuffix ".qcow2" originalImage.name);
|
||||
|
|
@ -27,6 +30,8 @@ let
|
|||
resultImg = "./disk.qcow2";
|
||||
hasScript = script != "";
|
||||
hasSmbios = smbios != null;
|
||||
pe = originalImage.pe or (throw "vmix: image ${originalImage.name} carries no PE (built by an older makeImage?)");
|
||||
volumeName = originalImage.volumeName or "Macintosh HD";
|
||||
|
||||
model = originalImage.model or "MacPro7,1";
|
||||
seed = if hasSmbios && (smbios.seed or null) != null then smbios.seed else null;
|
||||
|
|
@ -45,46 +50,60 @@ let
|
|||
inherit mac uuid;
|
||||
} // builtins.removeAttrs smbios [ "seed" "mac" "uuid" "model" ])
|
||||
else originalImage.opencore;
|
||||
# PE boot disk: serial console, and an OpenCore ScanPolicy that only allows
|
||||
# HFS+ volumes on SATA (= the PE), so the image's own macOS is never booted.
|
||||
# 0x10203 = FILE_SYSTEM_LOCK | DEVICE_LOCK | ALLOW_FS_HFS | ALLOW_DEVICE_SATA
|
||||
bootDisk = makeBootDisk {
|
||||
name = "${name}-${originalImageName}-pe";
|
||||
esp = originalImage.opencore;
|
||||
bootArgs = "keepsyms=1 serial=3 -v";
|
||||
scanPolicy = 66051;
|
||||
};
|
||||
|
||||
runScript = pkgs.writeText "${name}-vmix-run.sh" ''
|
||||
#!/bin/sh
|
||||
runScript = pkgs.writeText "${name}-run.sh" ''
|
||||
#!/bin/bash
|
||||
. /Volumes/VMIX/pe-lib.sh
|
||||
echo "=== vmix: ${name} ==="
|
||||
pe_mount_target || pe_fail "could not mount the target volumes"
|
||||
${script}
|
||||
pe_unmount_target
|
||||
'';
|
||||
vmixVol = makeVmixVolume {
|
||||
name = "${name}-${originalImageName}";
|
||||
files = [ { source = runScript; name = "vmix-run.sh"; } ] ++ files;
|
||||
files = [
|
||||
{ source = runScript; name = "run.sh"; }
|
||||
{ source = ../guest/pe-lib.sh; name = "pe-lib.sh"; }
|
||||
] ++ files;
|
||||
};
|
||||
driverPython = pkgs.python3.withPackages (p: [ p.pillow ]);
|
||||
|
||||
bootCommands = lib.optionalString hasScript ''
|
||||
cp ${vmixVol} vmix.img
|
||||
chmod +w vmix.img
|
||||
cat > vmix.conf <<CONF
|
||||
VOLUME_NAME="${volumeName}"
|
||||
BUILD_DATE="$(date -u +%m%d%H%M%Y.%S)"
|
||||
CONF
|
||||
guestfish -a vmix.img -m /dev/sda1 upload vmix.conf /vmix.conf
|
||||
qemu-img create -q -f qcow2 -F raw -b ${pe} pe.qcow2
|
||||
qemu-img create -q -f qcow2 -F raw -b ${bootDisk}/boot.img ocboot.qcow2
|
||||
cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd
|
||||
chmod +w vars.fd
|
||||
|
||||
VMIX_DISPLAY="-display none"
|
||||
${lib.optionalString (vncDisplay != null) ''VMIX_DISPLAY="-display none -vnc ${vncDisplay}"''}
|
||||
${lib.optionalString (vncDisplay == null) ''
|
||||
VMIX_DF=$(ls -t /tmp/.vmix-display-* 2>/dev/null | head -1)
|
||||
if [ -n "$VMIX_DF" ] && [ "$(stat -c %s "$VMIX_DF")" -lt 256 ] && ! grep -q -P '[^\x20-\x7e\n]' "$VMIX_DF"; then
|
||||
export DISPLAY=$(tr -d '\n' < "$VMIX_DF")
|
||||
export HOME=$(mktemp -d)
|
||||
export XDG_RUNTIME_DIR=$HOME
|
||||
export SDL_VIDEODRIVER=x11
|
||||
VMIX_DISPLAY="-display sdl"
|
||||
fi
|
||||
''}
|
||||
|
||||
echo "=== vmix: booting ${originalImageName} for ${name} ==="
|
||||
python3 ${vmDriver} --mode boot --name "${name}-${originalImageName}" --timeout ${toString timeout} --progress-file ${resultImg} -- \
|
||||
echo "=== vmix: running ${name} in the PE against ${originalImageName} ==="
|
||||
python3 ${vmDriver} --mode pe --name "${name}-${originalImageName}" --timeout ${toString timeout} \
|
||||
--serial-log serial.log --progress-file ${resultImg} -- \
|
||||
qemu-system-x86_64 $VMIX_DISPLAY \
|
||||
${qemu.machineArgs { inherit cpu smp memSize; }} \
|
||||
${qemu.firmwareArgs "vars.fd"} \
|
||||
${qemu.sataDrive { id = "system"; port = 0; file = resultImg; }} \
|
||||
${qemu.sataDrive { id = "vmix"; port = 1; file = "vmix.img"; format = "raw"; }} \
|
||||
${qemu.netArgs { mac = originalImage.macAddress; }} \
|
||||
|| { echo "vmix: VM failed during ${name} (see /tmp/vmix-macos/${name}-${originalImageName})"; exit 1; }
|
||||
${qemu.serialArgs "serial.log"} \
|
||||
${qemu.sataDrive { id = "opencore"; port = 0; file = "ocboot.qcow2"; }} \
|
||||
${qemu.sataDrive { id = "pe"; port = 1; file = "pe.qcow2"; }} \
|
||||
${qemu.sataDrive { id = "system"; port = 2; file = resultImg; }} \
|
||||
${qemu.sataDrive { id = "vmix"; port = 3; file = "vmix.img"; format = "raw"; }} \
|
||||
|| { echo "vmix: PE failed during ${name} (see /tmp/vmix-macos/${name}-${originalImageName})"; exit 1; }
|
||||
|
||||
${vmixReadback "vmix.img"}
|
||||
[ "$STATUS" = "0" ] || { echo "vmix: ${name} script failed (status '$STATUS')"; exit 1; }
|
||||
|
|
@ -92,7 +111,7 @@ let
|
|||
'';
|
||||
|
||||
builtImage = pkgs.runCommand customImageName ({
|
||||
nativeBuildInputs = with pkgs; [ pkgs.qemu mtools driverPython libguestfs-with-appliance ];
|
||||
nativeBuildInputs = with pkgs; [ pkgs.qemu driverPython libguestfs-with-appliance ];
|
||||
requiredSystemFeatures = [ "kvm" ];
|
||||
} // lib.optionalAttrs impure { __noChroot = true; }) ''
|
||||
qemu-img create -q -f qcow2 -b ${originalImage} -F qcow2 ${resultImg}
|
||||
|
|
@ -102,4 +121,4 @@ let
|
|||
mv ${resultImg} $out
|
||||
'';
|
||||
in
|
||||
builtImage // { _vmixOsType = "macos"; macAddress = mac; opencore = esp; model = esp.model or model; }
|
||||
builtImage // { _vmixOsType = "macos"; macAddress = mac; opencore = esp; model = esp.model or model; inherit pe volumeName; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue