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
136
cli.nix
136
cli.nix
|
|
@ -1,5 +1,9 @@
|
|||
# vmix CLI — build, copy, and run Windows images
|
||||
{ pkgs, self, system }:
|
||||
# vmix CLI — build, copy, and run Windows / macOS images
|
||||
{ pkgs, self, system, vmixLib }:
|
||||
let
|
||||
macosQemu = vmixLib.macos.qemu;
|
||||
macserial = vmixLib.macos.macserial;
|
||||
in
|
||||
pkgs.writeShellScriptBin "vmix" ''
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -8,23 +12,30 @@ pkgs.writeShellScriptBin "vmix" ''
|
|||
echo " vmix build --image <path> [--generalize key=val,...] [--out-link PATH]"
|
||||
echo " vmix copy --image <path> [--generalize key=val,...] --to-disk /dev/sdX"
|
||||
echo " vmix copy --image <path> [--generalize key=val,...] --to-remote-disk user@host:/dev/sdX"
|
||||
echo " vmix run <qcow2-file> [--mem 4096] [--smp 4] [--ahci]"
|
||||
echo " vmix run <qcow2-file> [--mem 4096] [--smp 4] [--ahci] [--macos] [--vnc :N] [--mac XX:..]"
|
||||
echo " vmix macserial [--model MacPro7,1]"
|
||||
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)"
|
||||
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 ""
|
||||
echo "Options:"
|
||||
echo " --image PATH Image path in vmixLib (e.g. windows.images.win10.laptop)"
|
||||
echo " --image PATH Image path in vmixLib (e.g. windows.images.win10.laptop,"
|
||||
echo " macos.images.tahoe.basic)"
|
||||
echo " --generalize KEY=VAL,... Finalize image with comma-separated options:"
|
||||
echo " username=User password= hostname=PC"
|
||||
echo " timezone=UTC bgColor=8e8cd8"
|
||||
echo " delay-oobe-run=true (OOBE + activation on real hardware)"
|
||||
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 " --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/AppleSMC flags, AHCI)"
|
||||
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 ""
|
||||
|
|
@ -32,9 +43,9 @@ pkgs.writeShellScriptBin "vmix" ''
|
|||
echo " vmix build --image windows.images.win10.laptop \\"
|
||||
echo " --generalize username=Sagar,password=secret,hostname=LAPTOP"
|
||||
echo ""
|
||||
echo " vmix copy --image windows.images.win10.laptop \\"
|
||||
echo " --generalize username=Sagar,password=secret,hostname=LAPTOP \\"
|
||||
echo " --to-disk /dev/sda"
|
||||
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 ""
|
||||
echo " vmix copy --image windows.images.win10.laptop \\"
|
||||
echo " --generalize username=Sagar,password=secret,hostname=LAPTOP \\"
|
||||
|
|
@ -49,24 +60,49 @@ pkgs.writeShellScriptBin "vmix" ''
|
|||
COMMAND="$1"; shift
|
||||
|
||||
case "$COMMAND" in
|
||||
build|copy|run) ;;
|
||||
build|copy|run|macserial) ;;
|
||||
--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 <qcow2-file> [--mem 4096] [--smp 4] [--ahci]"; exit 1; }
|
||||
[[ ''${#} -lt 1 ]] && { echo "Error: vmix run <qcow2-file> [--mem 4096] [--smp 4] [--ahci] [--macos] [--vnc :N] [--mac XX:XX:XX:XX:XX:XX]"; exit 1; }
|
||||
RUN_INPUT="$1"; shift
|
||||
RUN_MEM=4096
|
||||
RUN_SMP=4
|
||||
RUN_AHCI=false
|
||||
RUN_MACOS=false
|
||||
RUN_VNC=""
|
||||
RUN_MAC=""
|
||||
while [[ ''${#} -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mem) RUN_MEM="$2"; shift 2 ;;
|
||||
--smp) RUN_SMP="$2"; shift 2 ;;
|
||||
--ahci) RUN_AHCI=true; shift ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
--mem) RUN_MEM="$2"; shift 2 ;;
|
||||
--smp) RUN_SMP="$2"; shift 2 ;;
|
||||
--ahci) RUN_AHCI=true; shift ;;
|
||||
--macos) RUN_MACOS=true; shift ;;
|
||||
--vnc) RUN_VNC="$2"; shift 2 ;;
|
||||
--mac) RUN_MAC="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
|
@ -74,7 +110,9 @@ pkgs.writeShellScriptBin "vmix" ''
|
|||
[[ ! -f "$RUN_IMAGE" ]] && { echo "Error: file not found: $RUN_IMAGE"; exit 1; }
|
||||
|
||||
VMIX_DISPLAY="-nographic"
|
||||
if [[ -n "''${DISPLAY:-}" ]]; then
|
||||
if [[ -n "$RUN_VNC" ]]; then
|
||||
VMIX_DISPLAY="-display none -vnc $RUN_VNC"
|
||||
elif [[ -n "''${DISPLAY:-}" ]]; then
|
||||
VMIX_DISPLAY="-display sdl"
|
||||
fi
|
||||
|
||||
|
|
@ -87,6 +125,30 @@ 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)"
|
||||
echo ""
|
||||
exec ${pkgs.qemu}/bin/qemu-system-x86_64 \
|
||||
$VMIX_DISPLAY \
|
||||
${macosQemu.deviceArgs} ${macosQemu.vgaArgs} \
|
||||
-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 ""
|
||||
|
||||
|
|
@ -106,7 +168,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) \
|
||||
-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) \
|
||||
-device virtio-serial-pci \
|
||||
-chardev spicevmc,id=vdagent,debug=0,name=vdagent \
|
||||
-device virtserialport,chardev=vdagent,name=com.redhat.spice.0
|
||||
|
|
@ -161,8 +223,16 @@ 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"
|
||||
echo "Image: $IMAGE_NAME ($OS_TYPE)"
|
||||
[[ -n "$GENERALIZE" ]] && echo "Generalize: $GENERALIZE"
|
||||
[[ -n "$TO_DISK" ]] && echo "To disk: $TO_DISK"
|
||||
[[ -n "$TO_REMOTE_DISK" ]] && echo "To remote: $TO_REMOTE_DISK"
|
||||
|
|
@ -191,6 +261,10 @@ 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 ""
|
||||
|
|
@ -224,6 +298,15 @@ 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
|
||||
|
||||
|
|
@ -281,13 +364,22 @@ 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)..."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue