The base image (macos.images.tahoe.upstream) now installs and boots end to end with no dependency on Apple's servers — proven on the KVM host: the finished qcow2 boots standalone (OpenCore from its own ESP) to the macOS 26.6.2 loginwindow. Install driving (vm-driver.py, screenshot + OCR over QMP): - map the whole InstallAssistant.pkg as a raw disk and dd it byte-exact into the app as SharedSupport.dmg (it is a pkgdmg: xar + koly footer — the bare xar member fails startosinstall with "pkgdmg is missing a footer") - offline install: no NIC + /etc/hosts blackhole of Apple install/verify endpoints so startosinstall's calls fail fast instead of hanging (SecureBootModel=Disabled allows the sealed-volume install offline) - keep the recovery display awake with a tiny mouse jiggle; coarse settle fingerprint so the cursor is not seen as a change - guest watchdog re-erases/retries a startosinstall attempt that stalls or runs too long (prepare is intermittently slow) - disk-aware boot watchdog: QMP system_reset only when the screen is dark AND the disk is idle (never interrupts a slow-but-working boot); recovery-restart if a post-prepare reboot lands back on recovery - detect the bright loginwindow and power the VM down (install complete); a black + disk-idle screen is treated as a completed halt - copy OpenCore into the image ESP so it boots standalone recovery.file pins a content-addressed local BaseSystem.dmg (Apple's CDN load-balances Sequoia/Tahoe during the rollout). fetchRecovery retries to the pinned hash when used instead. Not yet done: .generalize (user creation) — the first-boot agent LaunchDaemon is blocked by Ventura+ Background Task Management on headless boots; next step is offline user injection from the agent pkg postinstall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsESshRCoBoUVWV9qKURUF
105 lines
4.5 KiB
Nix
105 lines
4.5 KiB
Nix
# 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} --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; }
|
|
|
|
${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; }
|