vmix.nix/lib/images/macos/helpers/makeOpenCore.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

69 lines
3.5 KiB
Nix

# OpenCore EFI for the VM: OSX-KVM's proven ESP (OpenCore + Lilu/VirtualSMC/... kexts)
# with a config.plist rewritten for this image's SMBIOS identity.
# Output:
# $out/EFI/ BOOT/BOOTx64.efi + OC/ — copied into the image's ESP
# $out/boot.img raw GPT disk with that ESP, used to boot the installer
# $out/vmix.json model, serial, mlb, uuid, mac (also copied to EFI/vmix/)
# Serial + MLB come from macserial when not given (random per build); everything
# derived from `seed` (MAC, UUID) is deterministic so the NixOS module knows it.
{ pkgs, lib, upstream, macserial, qemu, ... }:
{ name ? "opencore",
model ? "MacPro7,1",
mac,
uuid,
serial ? null,
mlb ? null,
bootArgs ? "keepsyms=1",
resolution ? "1024x768",
showPicker ? true,
pickerTimeout ? 2,
extraConfig ? {},
memSize ? 8192, # RAM described in SMBIOS (MacPro7,1 wants 4 DIMMs)
}:
let
ocImage = pkgs.fetchurl { inherit (upstream.opencore.image) url sha256; name = "OSX-KVM-OpenCore.qcow2"; };
in
pkgs.runCommand "${name}-esp" {
nativeBuildInputs = with pkgs; [ _7zz python3 mtools dosfstools gptfdisk jq macserial ];
passthru = { inherit model mac uuid; };
} ''
echo "=== vmix: extracting OSX-KVM OpenCore ESP ==="
7zz x -y -oparts ${ocImage} 0.primary.img >/dev/null
7zz x -y -oesp parts/0.primary.img >/dev/null
[ -f esp/EFI/OC/config.plist ] || { echo "config.plist not found in OpenCore image"; exit 1; }
SERIAL="${toString serial}"; MLB="${toString mlb}"
if [ -z "$SERIAL" ] || [ -z "$MLB" ]; then
echo "=== vmix: generating serial/MLB for ${model} with macserial ==="
LINE=$(macserial --num 1 --model "${model}" 2>/dev/null | grep '|' | tail -1)
[ -n "$LINE" ] || { echo "macserial produced nothing for ${model}"; exit 1; }
[ -z "$SERIAL" ] && SERIAL=$(echo "$LINE" | awk -F' *\\| *' '{print $1}')
[ -z "$MLB" ] && MLB=$(echo "$LINE" | awk -F' *\\| *' '{print $2}')
fi
echo "model=${model} serial=$SERIAL mlb=$MLB uuid=${uuid} mac=${mac}"
mkdir -p $out/EFI/vmix
cp -r esp/EFI/BOOT esp/EFI/OC $out/EFI/
chmod -R u+w $out/EFI
# hide OpenCore's own launcher from its picker (otherwise it is the default entry and loops)
echo -n Disabled > $out/EFI/BOOT/.contentVisibility
python3 ${./oc-config.py} --base esp/EFI/OC/config.plist --esp esp --out $out/EFI/OC/config.plist \
--model "${model}" --serial "$SERIAL" --mlb "$MLB" --uuid "${uuid}" --mac "${mac}" \
--nic-path "${qemu.nicDevicePath}" --boot-args "${bootArgs}" --resolution "${resolution}" \
--show-picker "${lib.boolToString showPicker}" --timeout ${toString pickerTimeout} \
--extra-json ${lib.escapeShellArg (builtins.toJSON extraConfig)} --memory-mb ${toString memSize}
ocvalidate $out/EFI/OC/config.plist || echo "vmix: ocvalidate reported issues (OpenCore version may differ from validator), continuing"
jq -n --arg model "${model}" --arg serial "$SERIAL" --arg mlb "$MLB" --arg uuid "${uuid}" --arg mac "${mac}" \
'{model:$model, serial:$serial, mlb:$mlb, uuid:$uuid, mac:$mac}' > $out/vmix.json
cp $out/vmix.json $out/EFI/vmix/vmix.json
echo "=== vmix: building OpenCore boot disk ==="
# 64 MiB raw GPT disk, ESP from sector 2048 to the end (minus backup GPT)
truncate -s 64M $out/boot.img
sgdisk -n 1:2048:0 -t 1:EF00 -c 1:EFI $out/boot.img >/dev/null
SECTORS=$(( 64*1024*1024/512 - 2048 - 34 ))
mkfs.vfat -F 32 -n OPENCORE --offset 2048 $out/boot.img $(( SECTORS / 2 )) >/dev/null
mcopy -i $out/boot.img@@1M -s $out/EFI ::
mdir -i $out/boot.img@@1M ::EFI/OC >/dev/null
''