Add a macOS image pipeline mirroring the Windows one: unattended install,
generalization and user creation, driven end to end in QEMU on a KVM host.
lib/images/macos:
- makeOpenCore: OSX-KVM OpenCore ESP with a config.plist rewritten per image —
SMBIOS model + serial/MLB (macserial) + UUID + ROM=en0 MAC (built-in NIC pinned
to PciRoot(0x0)/Pci(0x12,0x0)) for Apple ID / iMessage / App Store; boot disk;
OpenCore self-entry hidden. ident.nix derives MAC+UUID from a seed so the NixOS
module and CLI know the NIC MAC at eval time.
- makeImage: one QEMU session driven by vm-driver.py (QMP + screenshot settle
detection + OCR of the menu bar) — boots the recovery via OpenCore, opens
Terminal (Ctrl-F2 -> Utilities -> Terminal), types the bootstrap command;
vmix-install.sh erases the disk as APFS, lays down the host-extracted installer
app skeleton + a byte-exact SharedSupport.dmg (raw disk mapped to that byte range
of the pkg, dd'd in — Recovery's xar truncates an 18 GB member), runs
startosinstall with the vmix agent pkg. OpenCore is then copied into the image's
ESP so it boots standalone with OVMF.
- customizeImage / templates: boot the image with a FAT-then-HFS+ VMIX volume; the
vmix agent (LaunchDaemon) runs a script as root, records status and powers off —
the macOS counterpart of Windows Audit Mode. generalize creates the admin user +
auto-login (kcpassword), suppresses Setup Assistant, sets hostname/timezone,
grows APFS, and assigns a fresh SMBIOS identity. Templates: noUpdates,
performance, remoteAccess (ssh + screen sharing).
- fetchRecovery: Apple recovery BaseSystem, retried until the pinned Tahoe build
(osrecovery load-balances Sequoia/Tahoe during the rollout). makeAgentPkg builds
a distribution flat pkg on Linux (xar+bom+cpio) for startosinstall --installpackage.
CLI: vmix build/copy/run for macOS (run --macos --vnc, reads the image's MAC from
its ESP), and a `vmix macserial` helper. NixOS module: disks.os.file carrying
_vmixOsType="macos" auto-enables the macOS QEMU profile (AppleSMC+OSK, Skylake
CPU spoof, AHCI system disk, VMware SVGA, pinned NIC); macos.{enable,cpu,mac}.
Status: proven through the installer prepare phase (SharedSupport.dmg mounts,
version 26.6.2 read, SU catalog loads). Two blockers remain, documented in
lib/images/macos/README.md: (1) startosinstall's OSISVerifyBaseSystemOperation
rejects the byte-identical plain-UDIF SharedSupport as "pkgdmg missing a footer"
in this Tahoe recovery/VM; (2) Apple's CDN unreliably serves the Tahoe recovery
during rollout (self-hosting the verified BaseSystem is the robust fix).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
68 lines
3.4 KiB
Nix
68 lines
3.4 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 ? {},
|
|
}:
|
|
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)}
|
|
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
|
|
''
|