macOS Tahoe VM images (OpenCore/QEMU), Apple-ID compatible, VNC
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
This commit is contained in:
parent
6a62a649bd
commit
242e48a5fc
35 changed files with 1842 additions and 36 deletions
105
lib/images/macos/helpers/customizeImage.nix
Normal file
105
lib/images/macos/helpers/customizeImage.nix
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# 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.
|
||||
#
|
||||
# Templates provide:
|
||||
# script — sh script run as root on the booted system
|
||||
# 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, ... }:
|
||||
originalImage: {
|
||||
name ? "",
|
||||
script ? "",
|
||||
files ? [],
|
||||
smbios ? null,
|
||||
diskSize ? "",
|
||||
impure ? true,
|
||||
vncDisplay ? null,
|
||||
smp ? 4,
|
||||
memSize ? 4096,
|
||||
cpu ? qemu.defaultCpu,
|
||||
timeout ? 3600,
|
||||
}:
|
||||
let
|
||||
originalImageName = lib.strings.removeSuffix "-vmix" (lib.strings.removeSuffix ".qcow2" originalImage.name);
|
||||
customImageName = (if name != "" then name else "custom") + "-${originalImageName}-vmix.qcow2";
|
||||
resultImg = "./disk.qcow2";
|
||||
hasScript = script != "";
|
||||
hasSmbios = smbios != null;
|
||||
|
||||
model = originalImage.model or "MacPro7,1";
|
||||
seed = if hasSmbios && (smbios.seed or null) != null then smbios.seed else null;
|
||||
mac = if !hasSmbios then originalImage.macAddress
|
||||
else if (smbios.mac or null) != null then smbios.mac
|
||||
else if seed != null then ident.macFromSeed seed
|
||||
else originalImage.macAddress;
|
||||
uuid = if !hasSmbios then null
|
||||
else if (smbios.uuid or null) != null then smbios.uuid
|
||||
else if seed != null then ident.uuidFromSeed seed
|
||||
else originalImage.opencore.uuid;
|
||||
esp = if hasSmbios
|
||||
then makeOpenCore ({
|
||||
name = "${name}-${originalImageName}-opencore";
|
||||
model = smbios.model or model;
|
||||
inherit mac uuid;
|
||||
} // builtins.removeAttrs smbios [ "seed" "mac" "uuid" "model" ])
|
||||
else originalImage.opencore;
|
||||
|
||||
runScript = pkgs.writeText "${name}-vmix-run.sh" ''
|
||||
#!/bin/sh
|
||||
echo "=== vmix: ${name} ==="
|
||||
${script}
|
||||
'';
|
||||
vmixVol = makeVmixVolume {
|
||||
name = "${name}-${originalImageName}";
|
||||
files = [ { source = runScript; name = "vmix-run.sh"; } ] ++ files;
|
||||
};
|
||||
driverPython = pkgs.python3.withPackages (p: [ p.pillow ]);
|
||||
|
||||
bootCommands = lib.optionalString hasScript ''
|
||||
cp ${vmixVol} vmix.img
|
||||
chmod +w vmix.img
|
||||
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} -- \
|
||||
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; }
|
||||
|
||||
${vmixReadback "vmix.img"}
|
||||
[ "$STATUS" = "0" ] || { echo "vmix: ${name} script failed (status '$STATUS')"; exit 1; }
|
||||
echo "=== vmix: ${name} complete ==="
|
||||
'';
|
||||
|
||||
builtImage = pkgs.runCommand customImageName ({
|
||||
nativeBuildInputs = with pkgs; [ pkgs.qemu mtools driverPython libguestfs-with-appliance ];
|
||||
requiredSystemFeatures = [ "kvm" ];
|
||||
} // lib.optionalAttrs impure { __noChroot = true; }) ''
|
||||
qemu-img create -q -f qcow2 -b ${originalImage} -F qcow2 ${resultImg}
|
||||
[ -n "${diskSize}" ] && qemu-img resize ${resultImg} ${diskSize}
|
||||
${bootCommands}
|
||||
${lib.optionalString hasSmbios (installBootloader { inherit esp; image = resultImg; })}
|
||||
mv ${resultImg} $out
|
||||
'';
|
||||
in
|
||||
builtImage // { _vmixOsType = "macos"; macAddress = mac; opencore = esp; model = esp.model or model; }
|
||||
Loading…
Add table
Add a link
Reference in a new issue