# Build a pre-installed macOS qcow2 with an unattended install driven from the # vmix PE (Apple's Recovery + one LaunchDaemon, see makeRecoveryPE): # OpenCore boots the PE → its hook runs /Volumes/VMIX/run.sh (vmix-install.sh) # → erase the disk, rebuild the installer app from installer-app.tar + the # SharedSupport raw disk, startosinstall → the installer reboots through its # phases → the installed system's first boot reaches the loginwindow → the # driver powers it off. No GUI is driven; progress is read from the serial # console and screenshots (brightness). Fully offline: no NIC is attached. # Then OpenCore is copied into the image's own ESP so it boots with plain OVMF. # Apply templates with customizeImageFold, then .generalize. { pkgs, lib, qemu, ident, installerPayload, makeOpenCore, makeBootDisk, makeVmixVolume, installBootloader, vmixReadback, vmDriver, ... }: { name ? "macos", installer, # InstallAssistant.pkg pe, # makeRecoveryPE output for the same macOS version diskSize ? "128G", volumeName ? "Macintosh HD", smp ? 4, memSize ? 8192, cpu ? qemu.defaultCpu, model ? "MacPro7,1", # SMBIOS model; must be supported by the installed macOS seed ? name, # MAC address + SystemUUID are derived from this bootArgs ? "keepsyms=1", vncDisplay ? null, # e.g. ":10" to watch the install on port 5910 timeout ? 4 * 3600, # seconds for the whole install extraOpenCoreConfig ? {}, # merged into config.plist installNetwork ? false, # attach a NIC during the install (default: offline) }: let mac = ident.macFromSeed seed; uuid = ident.uuidFromSeed seed; esp = makeOpenCore { name = "${name}-opencore"; inherit model mac uuid bootArgs memSize; extraConfig = extraOpenCoreConfig; }; # build-time boot disk: same identity, plus serial console + verbose boot bootDisk = makeBootDisk { name = "${name}-install"; inherit esp; bootArgs = "${bootArgs} serial=3 -v"; }; payload = installerPayload { inherit name; pkg = installer; }; vmixVol = makeVmixVolume { inherit name; size = "512M"; files = [ { source = ../guest/vmix-install.sh; name = "run.sh"; } { source = ../guest/pe-lib.sh; name = "pe-lib.sh"; } { source = "${payload}/installer-app.tar"; name = "installer-app.tar"; } ]; }; driverPython = pkgs.python3.withPackages (p: [ p.pillow ]); drv = pkgs.runCommand "${name}-vmix.qcow2" { __noChroot = true; requiredSystemFeatures = [ "kvm" ]; nativeBuildInputs = with pkgs; [ pkgs.qemu jq driverPython libguestfs-with-appliance ]; } '' echo "=== vmix: creating ${diskSize} disk ===" qemu-img create -f qcow2 disk.qcow2 ${diskSize} # store files are read-only and AHCI needs writable nodes: qcow2 overlays 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 # Apple's postinstall hardlinks the WHOLE InstallAssistant.pkg as # Contents/SharedSupport/SharedSupport.dmg: the pkg is a "pkgdmg" (xar + koly # trailer whose DataForkOffset points at the dmg inside). startosinstall's # OSISVerifyBaseSystemOperation reads that footer, so the extracted xar member # alone fails with "pkgdmg is missing a footer". Expose the whole pkg as a raw # disk (zero host copy; qcow2 needs a 512-aligned size, the guest dd's PKG_BYTES). PKG_BYTES=$(stat -c %s ${installer}) PKG_DISK=$(( (PKG_BYTES + 511) / 512 * 512 )) qemu-img create -q -f qcow2 -F raw -b "json:{\"driver\":\"raw\",\"size\":$PKG_DISK,\"file\":{\"driver\":\"file\",\"filename\":\"${installer}\"}}" sharedsupport.qcow2 cp ${vmixVol} vmix.img chmod +w vmix.img TARGET_BYTES=$(qemu-img info --output=json disk.qcow2 | jq '."virtual-size"') cat > vmix.conf <