diff --git a/cli.nix b/cli.nix index b3d9f8f..d1a234a 100644 --- a/cli.nix +++ b/cli.nix @@ -1,9 +1,5 @@ -# vmix CLI — build, copy, and run Windows / macOS images -{ pkgs, self, system, vmixLib }: -let - macosQemu = vmixLib.macos.qemu; - macserial = vmixLib.macos.macserial; -in +# vmix CLI — build, copy, and run Windows images +{ pkgs, self, system }: pkgs.writeShellScriptBin "vmix" '' set -euo pipefail @@ -12,34 +8,23 @@ pkgs.writeShellScriptBin "vmix" '' echo " vmix build --image [--generalize key=val,...] [--out-link PATH]" echo " vmix copy --image [--generalize key=val,...] --to-disk /dev/sdX" echo " vmix copy --image [--generalize key=val,...] --to-remote-disk user@host:/dev/sdX" - echo " vmix run [--mem 4096] [--smp 4] [--ahci] [--macos] [--vnc :N] [--mac XX:..]" - echo " vmix macserial [--model MacPro7,1]" + echo " vmix run [--mem 4096] [--smp 4] [--ahci]" echo "" echo "Commands:" - echo " build Build a vmix image (optionally generalized)" - echo " copy Build a vmix image and write it to a disk" - echo " run Boot a qcow2 image with QEMU (SDL if DISPLAY available, or --vnc)" - echo " macserial Generate a SMBIOS identity (serial, MLB, UUID, MAC) for --generalize" + echo " build Build a vmix image (optionally generalized)" + echo " copy Build a vmix image and write it to a disk" + echo " run Boot a qcow2 image with QEMU (SDL if DISPLAY available)" echo "" echo "Options:" - echo " --image PATH Image path in vmixLib (e.g. windows.images.win10.laptop," - echo " macos.images.tahoe.basic)" + echo " --image PATH Image path in vmixLib (e.g. windows.images.win10.laptop)" echo " --generalize KEY=VAL,... Finalize image with comma-separated options:" echo " username=User password= hostname=PC" - echo " timezone=UTC bgColor=8e8cd8 (Windows only)" - echo " delay-oobe-run=true (OOBE/Setup Assistant on real hardware)" - echo " macOS SMBIOS: model=MacPro7,1 serial=... mlb=... uuid=... mac=... seed=..." + echo " timezone=UTC bgColor=8e8cd8" + echo " delay-oobe-run=true (OOBE + activation on real hardware)" echo " --to-disk DEVICE Write to local disk and expand partitions" echo " --to-remote-disk SSH:DEV Stream to remote disk via SSH and expand partitions" echo " e.g. root@10.10.10.100:/dev/sda" echo " --ahci Use AHCI storage for vmix run (for laptop images)" - echo " --macos macOS image for vmix run (OpenCore/VirtualSMC flags, AHCI)" - echo " --applesmc with --macos: add QEMU's isa-applesmc (images built before 2026-09-09)" - echo " --share DIR with --macos: virtio-fs share (first: /Volumes/My Shared Files); repeatable" - echo " --home FILE with --macos: persistent home volume (qcow2, created+formatted if missing)" - echo " --qga PATH with --macos: guest agent socket path (default /tmp/vmix-qga-.sock)" - echo " --vnc DISPLAY VNC instead of SDL for vmix run, e.g. :10 (port 5910) or 0.0.0.0:10" - echo " --mac ADDR NIC MAC for vmix run (macOS: read from the image's ESP by default)" echo " -y, --yes Skip disk write confirmation" echo " --out-link PATH Symlink for the build result (default: ./result)" echo "" @@ -47,9 +32,9 @@ pkgs.writeShellScriptBin "vmix" '' echo " vmix build --image windows.images.win10.laptop \\" echo " --generalize username=Sagar,password=secret,hostname=LAPTOP" echo "" - echo " vmix build --image macos.images.tahoe.basic \\" - echo " --generalize username=sagar,password=secret,hostname=MAC,timezone=Europe/Zurich" - echo " vmix run ./result --macos --vnc :10 --mem 8192" + echo " vmix copy --image windows.images.win10.laptop \\" + echo " --generalize username=Sagar,password=secret,hostname=LAPTOP \\" + echo " --to-disk /dev/sda" echo "" echo " vmix copy --image windows.images.win10.laptop \\" echo " --generalize username=Sagar,password=secret,hostname=LAPTOP \\" @@ -64,58 +49,24 @@ pkgs.writeShellScriptBin "vmix" '' COMMAND="$1"; shift case "$COMMAND" in - build|copy|run|macserial) ;; + build|copy|run) ;; --help|-h) usage ;; *) echo "Unknown command: $COMMAND"; usage ;; esac - # --- macserial command --- - if [[ "$COMMAND" == "macserial" ]]; then - MODEL="MacPro7,1" - while [[ ''${#} -gt 0 ]]; do - case "$1" in - --model) MODEL="$2"; shift 2 ;; - *) echo "Unknown option: $1"; exit 1 ;; - esac - done - LINE=$(${macserial}/bin/macserial --num 1 --model "$MODEL" 2>/dev/null | grep '|' | tail -1) - [[ -z "$LINE" ]] && { echo "Error: macserial produced nothing for model $MODEL"; exit 1; } - SERIAL=$(echo "$LINE" | awk -F' *\\| *' '{print $1}') - MLB=$(echo "$LINE" | awk -F' *\\| *' '{print $2}') - UUID=$(cat /proc/sys/kernel/random/uuid | tr a-f A-F) - MAC=$(printf '52:54:00:%02x:%02x:%02x' $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256))) - echo "model=$MODEL,serial=$SERIAL,mlb=$MLB,uuid=$UUID,mac=$MAC" - exit 0 - fi - # --- run command --- if [[ "$COMMAND" == "run" ]]; then - [[ ''${#} -lt 1 ]] && { echo "Error: vmix run [--mem 4096] [--smp 4] [--ahci] [--macos] [--vnc :N] [--mac XX:XX:XX:XX:XX:XX]"; exit 1; } + [[ ''${#} -lt 1 ]] && { echo "Error: vmix run [--mem 4096] [--smp 4] [--ahci]"; exit 1; } RUN_INPUT="$1"; shift RUN_MEM=4096 RUN_SMP=4 RUN_AHCI=false - RUN_MACOS=false - RUN_VNC="" - RUN_MAC="" - RUN_SHARES=() - RUN_HOME="" - RUN_HOME_IMAGE="macos.images.tahoe.upstream" - RUN_QGA="" while [[ ''${#} -gt 0 ]]; do case "$1" in - --mem) RUN_MEM="$2"; shift 2 ;; - --smp) RUN_SMP="$2"; shift 2 ;; - --ahci) RUN_AHCI=true; shift ;; - --macos) RUN_MACOS=true; shift ;; - --applesmc) RUN_APPLESMC=true; shift ;; - --share) RUN_SHARES+=("$2"); shift 2 ;; - --home) RUN_HOME="$2"; shift 2 ;; - --home-image) RUN_HOME_IMAGE="$2"; shift 2 ;; - --qga) RUN_QGA="$2"; shift 2 ;; - --vnc) RUN_VNC="$2"; shift 2 ;; - --mac) RUN_MAC="$2"; shift 2 ;; - *) echo "Unknown option: $1"; exit 1 ;; + --mem) RUN_MEM="$2"; shift 2 ;; + --smp) RUN_SMP="$2"; shift 2 ;; + --ahci) RUN_AHCI=true; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; esac done @@ -123,9 +74,7 @@ pkgs.writeShellScriptBin "vmix" '' [[ ! -f "$RUN_IMAGE" ]] && { echo "Error: file not found: $RUN_IMAGE"; exit 1; } VMIX_DISPLAY="-nographic" - if [[ -n "$RUN_VNC" ]]; then - VMIX_DISPLAY="-display none -vnc $RUN_VNC" - elif [[ -n "''${DISPLAY:-}" ]]; then + if [[ -n "''${DISPLAY:-}" ]]; then VMIX_DISPLAY="-display sdl" fi @@ -138,64 +87,6 @@ pkgs.writeShellScriptBin "vmix" '' echo "Memory: $RUN_MEM MB" echo "CPUs: $RUN_SMP" echo "Display: $VMIX_DISPLAY" - - if [[ "$RUN_MACOS" == "true" ]]; then - # OpenCore's ROM must match en0's MAC: the image records it in its ESP - if [[ -z "$RUN_MAC" ]]; then - RUN_MAC=$(${pkgs.libguestfs-with-appliance}/bin/guestfish --ro -a "$RUN_IMAGE" -m /dev/sda1 cat /EFI/vmix/vmix.json 2>/dev/null \ - | ${pkgs.jq}/bin/jq -r .mac 2>/dev/null || true) - [[ -z "$RUN_MAC" || "$RUN_MAC" == "null" ]] && { RUN_MAC="52:54:00:c9:18:27"; echo "Warning: could not read MAC from image ESP, using $RUN_MAC"; } - fi - echo "macOS: yes (MAC $RUN_MAC)" - # Apple's built-in QEMU guest agent: guest-exec as root over this socket - [[ -z "$RUN_QGA" ]] && RUN_QGA="/tmp/vmix-qga-$$.sock" - rm -f "$RUN_QGA" - echo "Agent: $RUN_QGA (guest-exec as root)" - # virtio-fs shares: the first one auto-mounts at /Volumes/My Shared Files, the - # others are mounted with: mount -t virtiofs /Volumes/ - MACOS_SHARE_ARGS="" - MACOS_MEM_ARGS="" - i=0 - for SHARE in "''${RUN_SHARES[@]}"; do - i=$((i + 1)); SOCK="/tmp/vmix-vfs-$$-$i.sock"; rm -f "$SOCK" - TAG=$([[ $i -eq 1 ]] && echo "${macosQemu.automountTag}" || echo "share$i") - ${pkgs.virtiofsd}/bin/virtiofsd --socket-path="$SOCK" --shared-dir "$SHARE" --cache auto --sandbox none >/dev/null 2>&1 & - for t in $(seq 1 50); do [[ -S "$SOCK" ]] && break; sleep 0.2; done - MACOS_SHARE_ARGS="$MACOS_SHARE_ARGS -chardev socket,id=vfs$i,path=$SOCK -device vhost-user-fs-pci,chardev=vfs$i,tag=$TAG" - MACOS_MEM_ARGS="-object memory-backend-memfd,id=vmix-mem,size=''${RUN_MEM}M,share=on -numa node,memdev=vmix-mem" - echo "Share: $SHARE -> $([[ $i -eq 1 ]] && echo '/Volumes/My Shared Files' || echo "mount -t virtiofs $TAG ...")" - done - # persistent home volume (virtio-blk); created + formatted APFS by the PE if missing - MACOS_HOME_ARGS="" - if [[ -n "$RUN_HOME" ]]; then - if [[ ! -e "$RUN_HOME" ]]; then - echo "Home: creating $RUN_HOME (64G qcow2) and formatting it as APFS 'vmix-home' via the PE of $RUN_HOME_IMAGE ..." - ${pkgs.qemu}/bin/qemu-img create -q -f qcow2 "$RUN_HOME" 64G - FMT=$(${pkgs.nix}/bin/nix build --no-link --print-out-paths --impure --expr "let l = (builtins.getFlake \"${self}\").lib.${system}; in l.macos.formatVolume { image = l.$RUN_HOME_IMAGE; }") || { echo "Error: could not build the formatter"; exit 1; } - "$FMT" "$RUN_HOME" qcow2 || exit 1 - fi - HOME_FMT=$(${pkgs.qemu}/bin/qemu-img info --output=json "$RUN_HOME" | ${pkgs.jq}/bin/jq -r .format) - MACOS_HOME_ARGS="-drive id=home,if=none,format=$HOME_FMT,file=$RUN_HOME -device virtio-blk-pci,drive=home" - echo "Home: $RUN_HOME (mounted at /Users by images generalized with persistHome)" - fi - echo "" - exec ${pkgs.qemu}/bin/qemu-system-x86_64 \ - $MACOS_MEM_ARGS $MACOS_SHARE_ARGS $MACOS_HOME_ARGS \ - -device virtio-serial-pci,id=vmix-vser -chardev socket,path="$RUN_QGA",server=on,wait=off,id=vmix-qga -device virtserialport,chardev=vmix-qga,name=org.qemu.guest_agent.0 \ - $VMIX_DISPLAY \ - ${macosQemu.deviceArgs} ${macosQemu.vgaArgs} \ - $([[ "$RUN_APPLESMC" == true ]] && echo '-device isa-applesmc,osk="${macosQemu.osk}"') \ - -accel kvm \ - -machine type=q35 \ - -cpu ${macosQemu.defaultCpu} \ - -smp "$RUN_SMP",sockets=1,cores="$RUN_SMP",threads=1 \ - -m "$RUN_MEM" \ - -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ - -drive if=pflash,format=raw,file=/tmp/vmix-run-vars-$$.fd \ - -drive id=os,if=none,format=qcow2,file="$RUN_IMAGE",snapshot=on -device ide-hd,bus=sata.0,drive=os \ - -netdev user,id=net0 -device virtio-net-pci,netdev=net0,mac="$RUN_MAC",bus=pcie.0,addr=${macosQemu.nicAddr} - fi - echo "AHCI: $RUN_AHCI" echo "" @@ -215,7 +106,7 @@ pkgs.writeShellScriptBin "vmix" '' else echo "-drive file=$RUN_IMAGE,format=qcow2,if=virtio,snapshot=on" fi) \ - -nic user,model=$(if [[ "$RUN_AHCI" == "true" ]]; then echo e1000; else echo virtio-net-pci; fi)$(if [[ -n "$RUN_MAC" ]]; then echo ",mac=$RUN_MAC"; fi) \ + -nic user,model=$(if [[ "$RUN_AHCI" == "true" ]]; then echo e1000; else echo virtio-net-pci; fi) \ -device virtio-serial-pci \ -chardev spicevmc,id=vdagent,debug=0,name=vdagent \ -device virtserialport,chardev=vdagent,name=com.redhat.spice.0 @@ -270,16 +161,8 @@ pkgs.writeShellScriptBin "vmix" '' FLAKE_DIR="${self}" - # OS type of the image (windows / macos / linux) decides the disk-writing steps - OS_TYPE=$(${pkgs.nix}/bin/nix eval --raw --impure --expr " - let - vmixLib = (builtins.getFlake \"$FLAKE_DIR\").lib.${system}; - image = vmixLib.$IMAGE_NAME; - in image._vmixOsType or \"linux\" - " 2>/dev/null || echo linux) - echo "=== vmix $COMMAND ===" - echo "Image: $IMAGE_NAME ($OS_TYPE)" + echo "Image: $IMAGE_NAME" [[ -n "$GENERALIZE" ]] && echo "Generalize: $GENERALIZE" [[ -n "$TO_DISK" ]] && echo "To disk: $TO_DISK" [[ -n "$TO_REMOTE_DISK" ]] && echo "To remote: $TO_REMOTE_DISK" @@ -308,10 +191,6 @@ pkgs.writeShellScriptBin "vmix" '' IMAGE_FILE=$(readlink -f "$OUT_LINK") echo "Built: $IMAGE_FILE" - if [[ "$OS_TYPE" == "macos" ]]; then - echo "Run it with: vmix run $OUT_LINK --macos --vnc :10" - fi - # --- write to local disk --- if [[ -n "$TO_DISK" ]]; then echo "" @@ -345,15 +224,6 @@ pkgs.writeShellScriptBin "vmix" '' echo "[3/5] Fixing GPT backup header..." ${pkgs.gptfdisk}/bin/sgdisk -e "$TO_DISK" - - if [[ "$OS_TYPE" == "macos" ]]; then - echo "[4/5] APFS container cannot be grown from Linux — skipped" - echo "[5/5] Grow it from macOS with: diskutil apfs resizeContainer disk0s2 0" - echo "" - echo "Done. $TO_DISK is ready to boot (OpenCore in the EFI partition; real Macs need no OpenCore)." - exit 0 - fi - # delete recovery partition if present, then resize Windows partition ${pkgs.gptfdisk}/bin/sgdisk -d 4 "$TO_DISK" 2>/dev/null || true @@ -411,22 +281,13 @@ pkgs.writeShellScriptBin "vmix" '' kill $NBD_PID 2>/dev/null || true rm -f "$NBD_SOCK" - echo "[3/5] Fixing GPT backup header..." - if [[ "$OS_TYPE" == "macos" ]]; then - ssh "$REMOTE_HOST" "nix-shell -p gptfdisk --run 'sgdisk -e $REMOTE_DISK'" - echo "[4/5] APFS container cannot be grown from Linux — skipped" - echo "[5/5] Grow it from macOS with: diskutil apfs resizeContainer disk0s2 0" - echo "" - echo "Done. $REMOTE_HOST:$REMOTE_DISK is ready to boot." - exit 0 - fi - if [[ "$REMOTE_DISK" == *nvme* ]] || [[ "$REMOTE_DISK" == *mmcblk* ]]; then REMOTE_WIN_PART="''${REMOTE_DISK}p3" else REMOTE_WIN_PART="''${REMOTE_DISK}3" fi + echo "[3/5] Fixing GPT backup header..." ssh "$REMOTE_HOST" "nix-shell -p gptfdisk --run 'sgdisk -e $REMOTE_DISK && sgdisk -d 4 $REMOTE_DISK 2>/dev/null || true'" echo "[4/5] Expanding Windows partition (partition 3)..." diff --git a/flake.nix b/flake.nix index b78796b..4ec2e5a 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,7 @@ lib.${system} = vmixLib; - packages.${system}.default = import ./cli.nix { inherit pkgs self system vmixLib; }; + packages.${system}.default = import ./cli.nix { inherit pkgs self system; }; apps.${system}.default = { type = "app"; diff --git a/lib/default.nix b/lib/default.nix index 8b4f0e9..2f1f2ed 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -4,6 +4,6 @@ let network = import ./network.nix { inherit pkgs lib; }; in { - inherit (images) linux windows macos; + inherit (images) linux windows; inherit network; } \ No newline at end of file diff --git a/lib/images/default.nix b/lib/images/default.nix index 5c07515..aecd364 100644 --- a/lib/images/default.nix +++ b/lib/images/default.nix @@ -2,5 +2,4 @@ { linux = (import ./linux) { inherit pkgs lib system; }; windows = (import ./windows) { inherit pkgs lib system; }; - macos = (import ./macos) { inherit pkgs lib system; }; -} +} \ No newline at end of file diff --git a/lib/images/macos/README.md b/lib/images/macos/README.md deleted file mode 100644 index 256fe0e..0000000 --- a/lib/images/macos/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# macOS images (Tahoe 26) - -Pre-installed, Apple-ID-capable macOS VM images built the same way as the -Windows ones: `makeImage` (unattended install) → templates → `.generalize` -(user, hostname, fresh SMBIOS identity). Runs on QEMU/KVM with OpenCore. - -``` -vmix build --image macos.images.tahoe.basic --generalize username=sagar,password=secret,hostname=MAC -vmix run ./result --macos --vnc :10 --mem 8192 -``` - -Nix: `macos.images.tahoe.{pe,upstream,basic,remote}` and -`.generalize { username; password; hostname; timezone; locale; seed; … }`. - -## How it works: the vmix "PE" - -Apple's Recovery (`BaseSystem.dmg`, a plain journaled HFS+ volume) with **one -LaunchDaemon added** (`makeRecoveryPE`): at boot it mounts a `VMIX` volume and -runs `run.sh` from it as root, records the exit status and powers off. That is -the whole automation surface — the equivalent of Windows PE + Autounattend: - -* **no GUI is driven**: no OCR, no keystrokes, no screen layouts to learn per - macOS version; the hook is a launchd plist, stable across releases (same idea - as AutoNBI/Imagr NetBoot images). -* **observable**: the guest prints `VMIX-*` markers to `/dev/console`, which the - build reads from QEMU's serial log (`boot-args serial=3 -v`). Kernel panics and - reboots show up there too. Screenshots are still taken for debugging. -* **offline**: no NIC during the install, and the guest blackholes Apple's - install/verify endpoints so `startosinstall` never waits on the network. The - only inputs are the pinned `InstallAssistant.pkg` and `BaseSystem.dmg`. -* **everything else happens offline from the PE too**: templates and generalize - mount the image's Data volume (rw) and System volume (ro) and edit them - (`dscl -f` for users, `plutil` for preferences) — the installed macOS is - never booted for customization, so nothing depends on launchd/BTM approval, - first-boot agents or auto-login inside the guest. One PE boot ≈ 30 s. - -### Pipeline - -1. `makeRecoveryPE` — BaseSystem.dmg → raw HFS+ image + `ch.vmix.pe` daemon. -2. `makeImage` — QEMU with: OpenCore boot disk (build variant with serial - console), the PE, the empty target disk, the VMIX volume (`vmix-install.sh`, - installer app skeleton) and the whole `InstallAssistant.pkg` mapped as a raw - disk. The guest script erases the target as APFS, unpacks the app and `dd`s - the pkg into it as `SharedSupport.dmg` (it is a "pkgdmg": xar + koly footer; - the bare xar member fails with "pkgdmg is missing a footer"), then runs - `startosinstall`, which reboots itself through the install phases. The - installed system's first boot ends at the loginwindow: the driver detects the - bright screen and powers the VM down. OpenCore is then copied into the image's - own ESP so it boots with plain OVMF. -3. `customizeImage` — boots the PE with the image attached (OpenCore - `ScanPolicy` restricted to HFS+ on SATA, so only the PE can boot) and runs the - template script with `$SYS`/`$DATA` mounted. `pe-lib.sh` has the helpers. -4. `templates/generalize.nix` — user (dscl, admin, home from the user template), - auto-login (`kcpassword`), Setup Assistant suppression, hostname, locale, - timezone, keyboard type, container resize, fresh SMBIOS via a new OpenCore - ESP (`serial`/`mlb` from macserial, MAC + UUID from `seed`). - -### Recovery source - -`recovery.file` in `upstream.json` points at a content-addressed store path for -the verified Tahoe `BaseSystem.dmg` (Apple's CDN load-balances Sequoia/Tahoe -during the rollout, so a plain fetch is non-deterministic). Reproduce it on any -host with `nix store add-path --name macos-tahoe-BaseSystem.dmg BaseSystem.dmg`. -Drop `recovery.file` to fetch from Apple instead (`fetchRecovery` retries until -the pinned hash matches). - -## Reliability - -Things QEMU does intermittently, and what handles each (all in `vm-driver.py` -and `vmix-install.sh`; every event is logged with a reason): - -* `startosinstall` prepare stalls or crawls — the guest kills and retries it on a - freshly erased target (free-space watchdog + time cap). -* the installer comes back to the PE instead of the install phase — the PE - counts boots and simply re-runs the install (max 3). -* the installed system hangs at the Apple logo on first boot — a `system_reset` - is issued only when the screen is dark and frozen **and** disk and serial - console are idle, so a slow-but-working boot is never interrupted. -* macOS `shutdown -h` halts to a black screen without an ACPI power-off — an - idle black screen counts as a completed halt. -* a kernel panic (seen on the serial console) resets the VM. -* a wedged run fails at the 4 h timeout instead of hanging. - -`tools/soak.sh macos.images.tahoe.upstream 3` rebuilds an image N -times and tabulates outcome, duration, boots, resets, panics and retries. -Measured 2026-09-09 on the build host (Ryzen 7 7840HS, ZFS), Tahoe 26.6.2, -VirtualSMC-only, PE install — 3 of 3 builds completed: - -| run | minutes | kernel boots | prepare tries | panics (self-recovered) | reboot deaths | -|-----|---------|--------------|---------------|-------------------------|---------------| -| 1 | 30 | 8 | 1 | 2 | 0 | -| 2 | 26 | 7 | 1 | 1 | 0 | -| 3 | 26 | 7 | 1 | 1 | 0 | - -What still happens: at roughly one in ten guest-initiated reboots the guest -either panics (GPF in launchd/kernel_task context shortly after `MACH Reboot` -or within the first 15 s of the next boot — tmpfs/APFS/zone corruption -signatures, i.e. memory or register state, not one driver) or never comes back -(dead after `IOPlatformHaltRestartAction`). XNU reboots itself after a panic; -the driver resets a dead guest after 60 s, so builds complete. A device bisect -(`tools`-style 10–30 PE reboots per variant: VMware SVGA vs std VGA, no HDA, -EHCI input, 1 vCPU) showed the rate is independent of the emulated devices and -of SMP; Haswell-noTSX does not boot Tahoe. Host: AMD Zen 4, kvm_amd, Intel -Skylake-Client vCPU model — the FPU-context-switch panic points at XSAVE state -handling on that combination. Not fixed; a `vmix run` VM that hangs on Restart -must be reset from the host. - -## Debugging - -`/tmp/vmix-macos//` on the build host: `driver.log`, `serial.log` -(kernel + `VMIX-*` markers), periodic PNG screenshots, `qmp.sock`. -`vmix-run.log` / `system-install.log` from the VMIX volume are printed at the -end of the build. Add `vncDisplay = ":10"` to watch. - -## QEMU profile - -`helpers/qemu.nix`: q35, `Skylake-Client` CPU spoof (works on AMD), -AppleSMC with the OSK, XHCI keyboard/tablet, AHCI disks, VMware SVGA, -virtio-net pinned to `PciRoot(0x0)/Pci(0x12,0x0)` so OpenCore marks it built-in -(en0, required for Apple ID / iMessage). SMBIOS `MacPro7,1` with four DIMMs -described (avoids the "Memory Modules Misconfigured" warning). - -OpenCore comes from OSX-KVM's proven ESP, with Lilu / VirtualSMC / -WhateverGreen replaced by current releases (`upstream.json` → `opencore.kexts`): -the versions OSX-KVM ships disable themselves on macOS 26, and without -VirtualSMC the guest's restart path panics on QEMU's SMC stub -(`SMCWDT smcWriteKey kSMCBadCommand`, nested panic after `MACH Reboot`). -For the same reason QEMU's `isa-applesmc` is not used any more: its presence -makes VirtualSMC step aside ("multiple devices present"); VirtualSMC carries -the OSK itself. Images built before this change still need the stub: -`vmix run --macos --applesmc`. RestrictEvents (`revpatch=memtab`) silences -MacPro7,1's "Memory Modules Misconfigured" at login. - -## Guest agent, shares, persistent home, online templates - -macOS 13+ ships **Apple's own QEMU guest agent** (`/usr/libexec/AppleQEMUGuestAgent`, -started by launchd when a virtio console port named `org.qemu.guest_agent.0` -appears). It is Apple-signed, needs no approval, and offers `guest-exec` as -root plus `guest-file-*`. vmix uses it everywhere an in-guest agent is needed: - -* `vmix run --macos` and the NixOS module attach it by default - (`/tmp/vmix-qga-.sock`, `/run/vmix/qga-.sock`); talk to it with any - QGA client, e.g. `printf '{"execute":"guest-exec","arguments":{"path":"/usr/bin/id","capture-output":true}}\n' | socat - UNIX-CONNECT:`. -* **online templates** (`bootScript`): `customizeImage` boots the image with the - agent, runs the script as root (network available, `as_user ` runs inside - the logged-in user's session), then shuts down through the agent. - `templates.software.script { name; script; }`, - `templates.software.homebrew { formulae; casks; }`, - `templates.profile.settings { hideWidgets; wallpaper; dockApps; dockAutohide; - darkMode; showHiddenFiles; }` (wallpaper via the pinned `desktoppr`; Apple - Events / `osascript` do not work headless — TCC automation consent). -* **offline software templates** run in the PE: `templates.software.pkg { name; - src; }` (`installer -target`), `templates.software.app { name; src; }`. - -`AppleVirtIO.kext` (x86 Tahoe) drives virtio-fs, 9p, block, console, input, -net, sound, balloon, vsock — QEMU's modern virtio-pci devices work as-is: - -* **shared folders**: virtio-fs (`virtiofsd` + `vhost-user-fs-pci`, shared - memory backend). The tag `com.apple.virtio-fs.automount` is mounted by macOS - itself at `/Volumes/My Shared Files`; further tags are mounted with - `mount -t virtiofs ` — the module does that through the guest agent - for every `shares.` beyond the first. `vmix run --macos --share DIR`. - (9p does not automount on macOS; the Linux `-virtfs` path is not used.) -* **ephemeral OS disk + persistent home**: `generalize { persistHome = true; }` - gives the account its home directory on an APFS volume labelled `vmix-home` - (`NFSHomeDirectory = /Volumes/vmix-home/`; macOS refuses mounts over - `/Users`, which is a firmlink). The host provides a virtio-blk disk - (`macos.homeDisk` in the module, `--home FILE` in the CLI: qcow2/raw file or - zvol) that `formatVolume` formats as APFS `vmix-home` by booting the PE for - ~35 s on first use; diskarbitrationd mounts it before login and loginwindow - creates the home directory there on first login. The OS disk can then run - with `snapshot=on` (`disks.os.persist = false`). -* **SPICE**: `-vga vmware` (or `std`) is kept as the display device — macOS has - no QXL/virtio-gpu driver; USB redirection channels work as for other guests - (`spice.usbRedir`); there is no vdagent for macOS (no clipboard sharing). - virtio keyboard/tablet (`AppleVirtIOInput`) are available as - `qemu.virtioInputArgs` but the USB HID pair is the default. diff --git a/lib/images/macos/default.nix b/lib/images/macos/default.nix deleted file mode 100644 index c6dd8bd..0000000 --- a/lib/images/macos/default.nix +++ /dev/null @@ -1,46 +0,0 @@ -{ pkgs, lib, system, ... }: -let - upstream = (lib.importJSON ./upstream.json).${system}; - macos = rec { - inherit upstream; - qemu = import ./helpers/qemu.nix { inherit pkgs lib; }; - ident = import ./helpers/ident.nix { inherit lib; }; - macserial = import ./helpers/macserial.nix { inherit pkgs upstream; }; - fetchRecovery = import ./helpers/fetchRecovery.nix { inherit pkgs upstream; }; - installerPayload = import ./helpers/installerPayload.nix { inherit pkgs lib; }; - makeOpenCore = import ./helpers/makeOpenCore.nix { inherit pkgs lib upstream macserial qemu; }; - makeBootDisk = import ./helpers/makeBootDisk.nix { inherit pkgs lib; }; - makeRecoveryPE = import ./helpers/makeRecoveryPE.nix { inherit pkgs lib; }; - makeVmixVolume = import ./helpers/makeVmixVolume.nix { inherit pkgs lib; }; - installBootloader = import ./helpers/installBootloader.nix { inherit pkgs lib; }; - vmixReadback = import ./helpers/vmix-readback.nix { inherit pkgs lib; }; - vmDriver = ./helpers/vm-driver.py; - makeImage = import ./helpers/makeImage.nix { - inherit pkgs lib qemu ident installerPayload makeOpenCore makeBootDisk makeVmixVolume installBootloader vmixReadback vmDriver; - }; - customizeImage = import ./helpers/customizeImage.nix { - inherit pkgs lib qemu ident makeVmixVolume makeOpenCore makeBootDisk installBootloader vmixReadback vmDriver; - }; - customizeImageFold = builtins.foldl' customizeImage; - formatVolume = import ./helpers/formatVolume.nix { inherit pkgs lib qemu makeVmixVolume makeBootDisk vmDriver; }; - templates = import ./templates { inherit pkgs lib; }; - }; - - tahoe = import ./tahoe { inherit pkgs lib system macos; }; - - # Recursively add .generalize to every image leaf (same shape as windows) - addGeneralize = val: - if val ? _vmixOsType then - val // { generalize = args: - let - templateArgs = builtins.removeAttrs args [ "vncDisplay" ]; - displayArgs = lib.optionalAttrs (args ? vncDisplay) { inherit (args) vncDisplay; }; - in macos.customizeImage val (macos.templates.generalize templateArgs // displayArgs); - } - else if builtins.isAttrs val then - lib.mapAttrs (_: addGeneralize) val - else val; -in -macos // { - images = addGeneralize { inherit tahoe; }; -} diff --git a/lib/images/macos/guest/ch.vmix.pe.plist b/lib/images/macos/guest/ch.vmix.pe.plist deleted file mode 100644 index a83065a..0000000 --- a/lib/images/macos/guest/ch.vmix.pe.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - Label - ch.vmix.pe - ProgramArguments - - /bin/bash - /usr/libexec/vmix/pe.sh - - RunAtLoad - - StandardOutPath - /dev/console - StandardErrorPath - /dev/console - - diff --git a/lib/images/macos/guest/kcpassword.py b/lib/images/macos/guest/kcpassword.py deleted file mode 100755 index 6403686..0000000 --- a/lib/images/macos/guest/kcpassword.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -# Encode a password as /etc/kcpassword (macOS auto-login). XOR with Apple's -# fixed key, zero padded to a multiple of 12 so a key byte terminates it. -import sys - -KEY = [0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F] - -pw = list(sys.argv[1].encode()) if len(sys.argv) > 1 else [] -pw += [0] * (12 - len(pw) % 12) -out = bytes(b ^ KEY[i % len(KEY)] for i, b in enumerate(pw)) -sys.stdout.buffer.write(out) diff --git a/lib/images/macos/guest/pe-lib.sh b/lib/images/macos/guest/pe-lib.sh deleted file mode 100644 index 08b09c1..0000000 --- a/lib/images/macos/guest/pe-lib.sh +++ /dev/null @@ -1,51 +0,0 @@ -# vmix PE helpers, sourced by run.sh scripts running in the recovery. -# Expects V=/Volumes/VMIX (set by pe.sh) and VOLUME_NAME from vmix.conf. -V=${V:-/Volumes/VMIX} -[ -f "$V/vmix.conf" ] && . "$V/vmix.conf" -VOLUME_NAME=${VOLUME_NAME:-Macintosh HD} - -pe_log() { echo "VMIX: $*"; } -pe_fail() { echo "VMIX-FAIL: $*"; exit 1; } - -# Mount the installed system's APFS volume group (System read-only, Data rw) and -# export SYS / DATA mount points plus SYS_ID / DATA_ID device identifiers. -pe_mount_target() { - local list; list=$(diskutil list) - DATA_ID=$(echo "$list" | awk -v n="APFS Volume $VOLUME_NAME - Data" 'index($0, n) {print $NF; exit}') - SYS_ID=$(echo "$list" | awk -v n="APFS Volume $VOLUME_NAME " '!/ - Data/ && index($0, n) {print $NF; exit}') - [ -n "$DATA_ID" ] && [ -n "$SYS_ID" ] || { pe_log "target volumes not found"; echo "$list"; return 1; } - diskutil mount "$SYS_ID" >/dev/null 2>&1 || true - diskutil mount "$DATA_ID" >/dev/null 2>&1 || true - SYS=$(diskutil info "$SYS_ID" | sed -n 's/^ *Mount Point: *//p') - DATA=$(diskutil info "$DATA_ID" | sed -n 's/^ *Mount Point: *//p') - [ -d "$DATA/private/var/db" ] || { pe_log "Data volume not mounted (SYS=[$SYS] DATA=[$DATA])"; return 1; } - pe_log "target mounted: SYS=[$SYS] DATA=[$DATA]" - export SYS DATA SYS_ID DATA_ID -} - -pe_unmount_target() { - sync - diskutil unmount "$DATA_ID" >/dev/null 2>&1 || true - diskutil unmount "$SYS_ID" >/dev/null 2>&1 || true -} - -# plist helpers on files of the (offline) target: create the file if missing. -pe_plist_set() { # FILE KEYPATH TYPE VALUE (TYPE: string|bool|integer|float) - local f=$1 k=$2 t=$3 v=$4 - [ -f "$f" ] || plutil -create xml1 "$f" - plutil -replace "$k" "-$t" "$v" "$f" -} -pe_plist_dict() { # FILE KEYPATH — make sure a dictionary exists at KEYPATH - local f=$1 k=$2 - [ -f "$f" ] || plutil -create xml1 "$f" - plutil -extract "$k" xml1 -o /dev/null "$f" >/dev/null 2>&1 || plutil -insert "$k" -dictionary "$f" -} -# launchd service override on the target (disabled.plist): pe_service LABEL true|false -pe_service_disabled() { - local f="$DATA/private/var/db/com.apple.xpc.launchd/disabled.plist" - mkdir -p "$(dirname "$f")" - pe_plist_set "$f" "$1" bool "$2" -} -# version of the installed system -pe_target_version() { plutil -extract ProductVersion raw -o - "$SYS/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null; } -pe_target_build() { plutil -extract ProductBuildVersion raw -o - "$SYS/System/Library/CoreServices/SystemVersion.plist" 2>/dev/null; } diff --git a/lib/images/macos/guest/pe.sh b/lib/images/macos/guest/pe.sh deleted file mode 100755 index ecb8f29..0000000 --- a/lib/images/macos/guest/pe.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# vmix PE hook. Runs as root from launchd when the patched Recovery boots -# (injected by makeRecoveryPE). If a VMIX volume is attached it runs -# /Volumes/VMIX/run.sh, records the exit status on the volume and powers off; -# without one it does nothing and the recovery behaves normally. -# Everything printed here goes to /dev/console, i.e. the host's serial log. -exec >/dev/console 2>&1 -echo "VMIX-PE: hook started $(date) uid=$(id -u)" -V=/Volumes/VMIX -i=0 -while [ ! -f "$V/run.sh" ] && [ $i -lt 90 ]; do - diskutil mount VMIX >/dev/null 2>&1 - sleep 2; i=$((i + 1)) -done -if [ ! -f "$V/run.sh" ]; then - echo "VMIX-PE: no VMIX volume, leaving the recovery alone" - exit 0 -fi -echo "VMIX-PE: VMIX mounted after $i retries" -caffeinate -dimsu -t 86400 >/dev/null 2>&1 & -[ -f "$V/vmix.conf" ] && . "$V/vmix.conf" -# certificate checks need a sane clock; a fresh VM RTC can be off -[ -n "${BUILD_DATE:-}" ] && date -u "$BUILD_DATE" >/dev/null 2>&1 && echo "VMIX-PE: clock set to $(date -u)" -export V -cd "$V" -echo "VMIX-PE: running run.sh" -/bin/bash "$V/run.sh" 2>&1 | tee "$V/vmix-run.log" -rc=${PIPESTATUS[0]} -echo "$rc" > "$V/vmix-run.status" -echo "VMIX-PE: run.sh exited $rc" -if [ -f "$V/vmix-reboot" ]; then - rm -f "$V/vmix-reboot"; sync - echo "VMIX-PE: rebooting as requested" - reboot - exit 0 -fi -sync; sleep 1 -diskutil unmount force "$V" >/dev/null 2>&1 -echo "VMIX-PE-DONE rc=$rc" -shutdown -h now diff --git a/lib/images/macos/guest/vmix-install.sh b/lib/images/macos/guest/vmix-install.sh deleted file mode 100644 index 8374331..0000000 --- a/lib/images/macos/guest/vmix-install.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash -# vmix unattended macOS install, run by the PE hook (pe.sh) as root in the -# Recovery with /Volumes/VMIX mounted (V). Needs vmix.conf: TARGET_BYTES, -# PKG_BYTES, PKG_DISK_BYTES, APP_NAME, VOLUME_NAME. -# 1. find the target disk and the SharedSupport (InstallAssistant.pkg) disk by size -# 2. erase the target as APFS, unpack the installer app, dd the whole pkg into it -# as SharedSupport.dmg (a "pkgdmg", startosinstall checks its koly footer) -# 3. startosinstall prepares, then reboots itself into the install phase; the -# installed system's first boot ends at the loginwindow (the host powers off) -# Never returns on success; a return means failure (the PE records the status). -set -x -. "$V/pe-lib.sh" -fail() { - echo "VMIX-FAIL: $*" - cp /var/log/install.log "$V/system-install.log" 2>/dev/null - sync - exit 1 -} -# each boot into the PE with the install still pending is one attempt -ATTEMPT=$(( $(cat "$V/install.attempt" 2>/dev/null || echo 0) + 1 )) -echo "$ATTEMPT" > "$V/install.attempt"; sync -echo "VMIX-INSTALL: attempt $ATTEMPT (boot into the PE)" -[ "$ATTEMPT" -le 3 ] || fail "the installer keeps coming back to the PE ($ATTEMPT boots)" - -# --- 1. disks by exact size -disk_by_size() { - for d in $(diskutil list | grep -oE '^/dev/disk[0-9]+' | sort -u); do - if [ "$(diskutil info "$d" | sed -n 's/.*Disk Size:.*(\([0-9]*\) Bytes).*/\1/p')" = "$1" ]; then - echo "${d#/dev/}"; return 0 - fi - done - return 1 -} -TARGET=$(disk_by_size "$TARGET_BYTES") || fail "target disk of $TARGET_BYTES bytes not found" -SSDISK=$(disk_by_size "$PKG_DISK_BYTES") || fail "SharedSupport disk of $PKG_DISK_BYTES bytes not found" -echo "VMIX-INSTALL: target=$TARGET sharedsupport=$SSDISK" - -# --- 2. target volume + installer app (the pkg payload skeleton + SharedSupport.dmg) -VOL="/Volumes/$VOLUME_NAME" -APP="$VOL/$APP_NAME" -SS="$APP/Contents/SharedSupport/SharedSupport.dmg" -prepare_target() { - diskutil eraseDisk APFS "$VOLUME_NAME" GPT "$TARGET" || fail "eraseDisk" - [ -d "$VOL" ] || fail "$VOL not mounted after erase" - tar -xf "$V/installer-app.tar" -C "$VOL" || fail "untar installer app" - [ -x "$APP/Contents/Resources/startosinstall" ] || fail "startosinstall missing from $APP" - mkdir -p "$APP/Contents/SharedSupport" - FULL=$(( PKG_BYTES / 1048576 )); REM=$(( PKG_BYTES % 1048576 )) - echo "VMIX-INSTALL: copying SharedSupport.dmg ($PKG_BYTES bytes) from /dev/r$SSDISK" - dd if="/dev/r$SSDISK" of="$SS" bs=1048576 count=$FULL || fail "dd SharedSupport" - [ "$REM" -gt 0 ] && { dd if="/dev/r$SSDISK" bs=1048576 skip=$FULL count=1 | dd bs=1 count=$REM >> "$SS"; } || true - [ "$(stat -f %z "$SS")" = "$PKG_BYTES" ] || fail "SharedSupport.dmg size $(stat -f %z "$SS") != $PKG_BYTES" - tail -c 512 "$SS" | grep -qa koly || fail "SharedSupport.dmg has no koly footer" - chflags -h norestricted "$SS" 2>/dev/null || true - sync -} -prepare_target -SOI="$APP/Contents/Resources/startosinstall" -echo "VMIX-INSTALL: app ready, clock $(date -u)" - -# Offline install: no NIC is attached. Blackhole Apple's install/verify endpoints -# too, so osinstallersetupd's requests fail immediately instead of timing out. -for d in swscan.apple.com swcdn.apple.com swdist.apple.com swquery.apple.com \ - gs.apple.com gsa.apple.com gdmf.apple.com mesu.apple.com xp.apple.com \ - albert.apple.com captive.apple.com deviceservices-external.apple.com \ - identity.apple.com ppq.apple.com crl.apple.com ocsp.apple.com \ - ocsp2.apple.com valid.apple.com; do - echo "127.0.0.1 $d" >> /etc/hosts -done - -# --- 3. startosinstall prepares (~5 min) then reboots the machine itself into the -# install phase; it never returns on success. Prepare is intermittently slow in -# QEMU, so an attempt that stalls or runs too long is killed and retried on a -# freshly erased target. -run_soi() { "$SOI" --volume "$VOL" --agreetolicense --nointeraction --rebootdelay 5 "$@"; } -free_kb() { df -k "$VOL" 2>/dev/null | awk 'NR==2 {print $4}'; } -try=0 -while [ "$try" -lt 6 ]; do - try=$((try + 1)) - [ "$try" -gt 1 ] && prepare_target - echo "VMIX-INSTALL: startosinstall try $try" - run_soi 2>&1 & - SOI_PID=$! - last=$(free_kb); stalled=0; elapsed=0 - while kill -0 "$SOI_PID" 2>/dev/null; do - sleep 30; elapsed=$((elapsed + 30)) - now=$(free_kb) - if [ "$now" = "$last" ]; then stalled=$((stalled + 30)); else stalled=0; last=$now; fi - [ $((elapsed % 120)) -eq 0 ] && echo "VMIX-INSTALL: prepare running ${elapsed}s (stalled ${stalled}s)" - if [ "$stalled" -ge 240 ] || [ "$elapsed" -ge 600 ]; then - echo "VMIX-INSTALL: prepare too slow (stalled=${stalled}s elapsed=${elapsed}s), killing to retry" - kill -9 "$SOI_PID" 2>/dev/null; pkill -9 -f startosinstall 2>/dev/null - break - fi - done - wait "$SOI_PID" 2>/dev/null - echo "VMIX-INSTALL: startosinstall try $try ended without rebooting" - sleep 3 -done -fail "startosinstall did not complete after $try tries" diff --git a/lib/images/macos/helpers/customizeImage.nix b/lib/images/macos/helpers/customizeImage.nix deleted file mode 100644 index 6f4dc11..0000000 --- a/lib/images/macos/helpers/customizeImage.nix +++ /dev/null @@ -1,176 +0,0 @@ -# Customize a macOS image offline from the vmix PE: the recovery boots with the -# image and a VMIX volume attached, its hook runs `script` as root with the -# image's System (read-only) and Data (rw) volumes mounted at $SYS / $DATA, then -# powers off. The installed macOS itself is never booted, so nothing depends on -# launchd/BTM approval inside the guest. Counterpart of the Windows -# registry/audit flow. Optionally re-installs OpenCore with a new SMBIOS -# identity (`smbios`). -# -# Templates provide: -# script — sh script run as root in the PE (pe-lib.sh helpers available) -# bootScript — sh script run as root on the BOOTED image through Apple's QEMU -# guest agent (network, user session available; run after `script`) -# 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 -# network — attach a user-mode NIC for bootScript (default true) -{ pkgs, lib, qemu, ident, makeVmixVolume, makeOpenCore, makeBootDisk, installBootloader, vmixReadback, vmDriver, ... }: -originalImage: { - name ? "", - script ? "", - bootScript ? "", - network ? true, - files ? [], - smbios ? null, - diskSize ? "", - impure ? true, - vncDisplay ? null, - smp ? 4, - memSize ? 4096, - cpu ? qemu.defaultCpu, - timeout ? 1800, - machineArgs ? null, # override qemu.machineArgs (device experiments) -}: -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 != ""; - hasBootScript = bootScript != ""; - hasSmbios = smbios != null; - pe = originalImage.pe or (throw "vmix: image ${originalImage.name} carries no PE (built by an older makeImage?)"); - volumeName = originalImage.volumeName or "Macintosh HD"; - - 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; - # PE boot disk: serial console, and an OpenCore ScanPolicy that only allows - # HFS+ volumes on SATA (= the PE), so the image's own macOS is never booted. - # 0x10203 = FILE_SYSTEM_LOCK | DEVICE_LOCK | ALLOW_FS_HFS | ALLOW_DEVICE_SATA - bootDisk = makeBootDisk { - name = "${name}-${originalImageName}-pe"; - esp = originalImage.opencore; - bootArgs = "keepsyms=1 serial=3 -v"; - scanPolicy = 66051; - }; - - runScript = pkgs.writeText "${name}-run.sh" '' - #!/bin/bash - . /Volumes/VMIX/pe-lib.sh - echo "=== vmix: ${name} ===" - pe_mount_target || pe_fail "could not mount the target volumes" - ${script} - pe_unmount_target - ''; - vmixVol = makeVmixVolume { - name = "${name}-${originalImageName}"; - files = [ - { source = runScript; name = "run.sh"; } - { source = ../guest/pe-lib.sh; name = "pe-lib.sh"; } - ] ++ files; - }; - bootRunScript = pkgs.writeText "${name}-boot.sh" '' - #!/bin/bash - # runs as root on the booted system (guest-exec); VMIX is mounted at $V - V=/Volumes/VMIX - echo "=== vmix (online): ${name} ===" - CONSOLE_USER=$(stat -f %Su /dev/console 2>/dev/null) - CONSOLE_UID=$(id -u "$CONSOLE_USER" 2>/dev/null) - export V CONSOLE_USER CONSOLE_UID - # run something inside the logged-in user's GUI session - as_user() { launchctl asuser "$CONSOLE_UID" sudo -u "$CONSOLE_USER" "$@"; } - ${bootScript} - ''; - bootVol = makeVmixVolume { - name = "${name}-${originalImageName}-boot"; - files = [ { source = bootRunScript; name = "run.sh"; } ] ++ files; - }; - driverPython = pkgs.python3.withPackages (p: [ p.pillow ]); - - bootCommands = lib.optionalString hasScript '' - cp ${vmixVol} vmix.img - chmod +w vmix.img - cat > vmix.conf </dev/null 2>&1; [ -f /Volumes/VMIX/run.sh ] && break; sleep 2; done; [ -f /Volumes/VMIX/run.sh ] || { echo "no VMIX volume"; exit 9; }; bash /Volumes/VMIX/run.sh > /Volumes/VMIX/vmix-run.log 2>&1; rc=$?; echo $rc > /Volumes/VMIX/vmix-run.status; sync; cat /Volumes/VMIX/vmix-run.log; diskutil unmount force /Volumes/VMIX >/dev/null 2>&1; exit $rc' -- \ - qemu-system-x86_64 $VMIX_DISPLAY \ - ${if machineArgs != null then machineArgs else qemu.machineArgs { inherit cpu smp memSize; }} \ - ${qemu.firmwareArgs "vars-boot.fd"} \ - ${qemu.serialArgs "serial-boot.log"} \ - ${qemu.guestAgentArgs "$QGA_SOCK"} \ - ${qemu.sataDrive { id = "system"; port = 0; file = resultImg; }} \ - ${qemu.sataDrive { id = "vmix"; port = 1; file = "vmix-boot.img"; format = "raw"; }} \ - ${lib.optionalString network (qemu.netArgs { mac = originalImage.macAddress; })} \ - || { echo "vmix: online step failed during ${name} (see /tmp/vmix-macos/${name}-${originalImageName}-online)"; exit 1; } - rm -f "$QGA_SOCK" - ${vmixReadback "vmix-boot.img"} - [ "$STATUS" = "0" ] || { echo "vmix: ${name} bootScript failed (status '$STATUS')"; exit 1; } - echo "=== vmix: ${name} (online) complete ===" - ''; - - builtImage = pkgs.runCommand customImageName ({ - nativeBuildInputs = with pkgs; [ pkgs.qemu 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} - ${onlineCommands} - ${lib.optionalString hasSmbios (installBootloader { inherit esp; image = resultImg; })} - mv ${resultImg} $out - ''; -in - builtImage // { _vmixOsType = "macos"; macAddress = mac; opencore = esp; model = esp.model or model; inherit pe volumeName; } diff --git a/lib/images/macos/helpers/fetchRecovery.nix b/lib/images/macos/helpers/fetchRecovery.nix deleted file mode 100644 index 8b3838f..0000000 --- a/lib/images/macos/helpers/fetchRecovery.nix +++ /dev/null @@ -1,40 +0,0 @@ -# macOS Recovery BaseSystem.dmg from Apple's recovery servers (osrecovery.apple.com) -# via OSX-KVM's fetch-macOS-v2.py. The server has no stable URL (session-based) and, -# during a macOS rollout, "latest" is load-balanced across CDN nodes serving DIFFERENT -# builds (e.g. Sequoia and Tahoe at once). So the download is non-deterministic: the -# builder retries until it gets the exact build pinned by sha256. The recovery MUST -# match the installer's major version or startosinstall rejects it as "damaged". -# When Apple retires this build, update recovery.sha256 (download once, check -# /System/Library/CoreServices/SystemVersion.plist reports the wanted version). -{ pkgs, upstream, ... }: -{ shortname, sha256 }: -let - script = pkgs.fetchurl { inherit (upstream.opencore.fetchRecoveryScript) url sha256; }; -in -pkgs.runCommand "macos-${shortname}-BaseSystem.dmg" { - nativeBuildInputs = [ pkgs.python3 pkgs.coreutils ]; - outputHashMode = "flat"; - outputHashAlgo = "sha256"; - outputHash = sha256; - SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - expected = sha256; -} '' - cp ${script} fetch-macOS-v2.py - sed -i 's/os.get_terminal_size().columns/80/' fetch-macOS-v2.py - want=$(nix-hash --type sha256 --to-base16 "$expected" 2>/dev/null || echo "$expected") - for attempt in $(seq 1 40); do - rm -f BaseSystem.dmg BaseSystem.chunklist - echo "=== vmix: fetching ${shortname} recovery (attempt $attempt) ===" - python3 fetch-macOS-v2.py --action download -s ${shortname} -o . -n BaseSystem || true - if [ -f BaseSystem.dmg ]; then - got=$(sha256sum BaseSystem.dmg | cut -d' ' -f1) - echo "got $got (want $want)" - [ "$got" = "$want" ] && { mv BaseSystem.dmg $out; exit 0; } - echo "=== vmix: wrong build (Apple is rotating builds during rollout), retrying ===" - fi - sleep 5 - done - echo "vmix: could not fetch the pinned ${shortname} recovery after 40 attempts." - echo "Apple may have retired build $want; download it manually and update recovery.sha256." - exit 1 -'' diff --git a/lib/images/macos/helpers/formatVolume.nix b/lib/images/macos/helpers/formatVolume.nix deleted file mode 100644 index d0d9e1a..0000000 --- a/lib/images/macos/helpers/formatVolume.nix +++ /dev/null @@ -1,56 +0,0 @@ -# Host-side script that formats a blank disk image as an APFS volume with a -# given label by booting the image's PE headless for ~30 s (Linux cannot write -# APFS). Used for the persistent home volume (`generalize { persistHome = true; }` -# mounts LABEL=