From 89a0673f540dc05adc5d44b9e2d585e03545c09a Mon Sep 17 00:00:00 2001 From: Git Sagar Date: Sun, 24 May 2026 11:01:39 -0300 Subject: [PATCH] SDL fallback, partition fix, lz4 remote streaming, delay-oobe-run SDL display: - try SDL, auto-fallback to headless if it fails (no crash) - SDL_VIDEODRIVER=x11 to avoid wayland socket path issues - suppress XDG_RUNTIME_DIR warnings Disk copy: - zap-all before writing to clear old partition tables - delete recovery partition (4) before resizing partition 3 - use parted resizepart (preserves partition GUID for BCD) - remote: nix-shell for sgdisk/parted/ntfsresize on target - remote: lz4 compression for faster streaming - remote: pv progress bar with disk size - -y/--yes flag to skip confirmation prompt Generalize: - delay-oobe-run=true defers OOBE + activation to real hardware - clean cached Autounattend from Windows\Panther before sysprep - taskkill sysprep.exe on first login (CopyProfile artifact) Co-Authored-By: Claude Opus 4.6 (1M context) --- flake.nix | 60 +++++++++++++------ lib/images/windows/helpers/customizeImage.nix | 24 ++++---- lib/images/windows/helpers/makeImage.nix | 20 ++++--- lib/images/windows/templates/generalize.nix | 12 +++- 4 files changed, 79 insertions(+), 37 deletions(-) diff --git a/flake.nix b/flake.nix index c8c027f..30da4b5 100644 --- a/flake.nix +++ b/flake.nix @@ -126,6 +126,7 @@ GENERALIZE="" TO_DISK="" TO_REMOTE_DISK="" + YES=false OUT_LINK="./result" while [[ ''${#} -gt 0 ]]; do @@ -134,6 +135,7 @@ --generalize) GENERALIZE="$2"; shift 2 ;; --to-disk) TO_DISK="$2"; shift 2 ;; --to-remote-disk) TO_REMOTE_DISK="$2"; shift 2 ;; + -y|--yes) YES=true; shift ;; --out-link) OUT_LINK="$2"; shift 2 ;; --help|-h) usage ;; *) echo "Unknown option: $1"; usage ;; @@ -212,17 +214,24 @@ echo "Target: $TO_DISK ($DISK_SIZE_GB GB)" echo "" echo "WARNING: This will DESTROY all data on $TO_DISK" - read -rp "Continue? [y/N] " confirm - [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } + if [[ "$YES" != "true" ]]; then + read -rp "Continue? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } + fi echo "" - echo "[1/4] Writing image to disk..." + echo "[1/5] Wiping partition table..." + ${pkgs.gptfdisk}/bin/sgdisk --zap-all "$TO_DISK" + + echo "[2/5] Writing image to disk..." ${pkgs.qemu}/bin/qemu-img convert -p -S 4k -f qcow2 -O raw "$IMAGE_FILE" "$TO_DISK" - echo "[2/4] Fixing GPT backup header..." + echo "[3/5] Fixing GPT backup header..." ${pkgs.gptfdisk}/bin/sgdisk -e "$TO_DISK" + # delete recovery partition if present, then resize Windows partition + ${pkgs.gptfdisk}/bin/sgdisk -d 4 "$TO_DISK" 2>/dev/null || true - echo "[3/4] Expanding Windows partition (partition 3) to fill disk..." + echo "[4/5] Expanding Windows partition (partition 3)..." ${pkgs.parted}/bin/parted -s "$TO_DISK" resizepart 3 100% if [[ "$TO_DISK" == *nvme* ]] || [[ "$TO_DISK" == *mmcblk* ]]; then @@ -231,7 +240,7 @@ WIN_PART="''${TO_DISK}3" fi - echo "[4/4] Expanding NTFS filesystem on $WIN_PART..." + echo "[5/5] Expanding NTFS filesystem on $WIN_PART..." ${pkgs.ntfs3g}/bin/ntfsresize --force --no-action "$WIN_PART" echo "y" | ${pkgs.ntfs3g}/bin/ntfsresize --force "$WIN_PART" @@ -252,19 +261,28 @@ echo "Disk: $REMOTE_DISK" echo "" echo "WARNING: This will DESTROY all data on $REMOTE_HOST:$REMOTE_DISK" - read -rp "Continue? [y/N] " confirm - [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } + if [[ "$YES" != "true" ]]; then + read -rp "Continue? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } + fi echo "" - echo "[1/4] Streaming image to remote disk..." - ${pkgs.qemu}/bin/qemu-img convert -f qcow2 -O raw "$IMAGE_FILE" /dev/stdout \ - | ssh "$REMOTE_HOST" "dd of=$REMOTE_DISK bs=4M status=progress oflag=direct" + echo "[1/5] Wiping partition table..." + ssh "$REMOTE_HOST" "sgdisk --zap-all $REMOTE_DISK" - echo "[2/4] Fixing GPT backup header..." - ssh "$REMOTE_HOST" "sgdisk -e $REMOTE_DISK" - - echo "[3/4] Expanding Windows partition (partition 3) to fill disk..." - ssh "$REMOTE_HOST" "parted -s $REMOTE_DISK resizepart 3 100%" + echo "[2/5] Streaming image to remote disk..." + NBD_SOCK="/tmp/vmix-nbd-$$.sock" + ${pkgs.qemu}/bin/qemu-nbd --read-only -f qcow2 -k "$NBD_SOCK" "$IMAGE_FILE" & + NBD_PID=$! + trap "kill $NBD_PID 2>/dev/null; rm -f $NBD_SOCK" EXIT + while [ ! -S "$NBD_SOCK" ]; do sleep 0.1; done + DISK_SIZE=$(${pkgs.libnbd}/bin/nbdinfo --size "nbd+unix:///?socket=$NBD_SOCK") + ${pkgs.libnbd}/bin/nbdcopy --request-size=4194304 "nbd+unix:///?socket=$NBD_SOCK" - \ + | ${pkgs.pv}/bin/pv -s "$DISK_SIZE" \ + | ${pkgs.lib.getBin pkgs.lz4}/bin/lz4 -1 - \ + | ssh "$REMOTE_HOST" "nix-shell -p lz4 --run 'lz4 -d - - | dd of=$REMOTE_DISK bs=4M iflag=fullblock oflag=direct conv=sparse'" + kill $NBD_PID 2>/dev/null || true + rm -f "$NBD_SOCK" if [[ "$REMOTE_DISK" == *nvme* ]] || [[ "$REMOTE_DISK" == *mmcblk* ]]; then REMOTE_WIN_PART="''${REMOTE_DISK}p3" @@ -272,8 +290,14 @@ REMOTE_WIN_PART="''${REMOTE_DISK}3" fi - echo "[4/4] Expanding NTFS filesystem on $REMOTE_WIN_PART..." - ssh "$REMOTE_HOST" "echo y | ntfsresize --force $REMOTE_WIN_PART" + 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)..." + ssh "$REMOTE_HOST" "nix-shell -p parted --run 'parted -s $REMOTE_DISK resizepart 3 100%'" + + echo "[5/5] Expanding NTFS filesystem on $REMOTE_WIN_PART..." + ssh "$REMOTE_HOST" "nix-shell -p ntfs3g --run 'echo y | ntfsresize --force $REMOTE_WIN_PART'" echo "" echo "Done. $REMOTE_HOST:$REMOTE_DISK is ready to boot." diff --git a/lib/images/windows/helpers/customizeImage.nix b/lib/images/windows/helpers/customizeImage.nix index 4e1e363..9f4f4a4 100644 --- a/lib/images/windows/helpers/customizeImage.nix +++ b/lib/images/windows/helpers/customizeImage.nix @@ -80,32 +80,36 @@ chmod +w vars.fd VMIX_DISPLAY="-nographic" - ${lib.optionalString (displayArg != null) ''VMIX_DISPLAY="${displayArg}"''} - ${lib.optionalString (displayArg == null) '' + ${lib.optionalString (vncDisplay != null) ''VMIX_DISPLAY="-vnc ${vncDisplay}"''} + ${lib.optionalString (vncDisplay == null) '' VMIX_DF=$(ls -t /tmp/.vmix-display-* 2>/dev/null | head -1) if [ -n "$VMIX_DF" ]; then export DISPLAY=$(cat "$VMIX_DF") export HOME=$(mktemp -d) export XDG_RUNTIME_DIR=$HOME + export SDL_VIDEODRIVER=x11 VMIX_DISPLAY="-display sdl" fi ''} echo "=== vmix: booting Audit Mode for ${name} ===" - timeout 1800 qemu-system-x86_64 \ - $VMIX_DISPLAY \ - -accel kvm \ - -m ${toString memSize} \ - -smp ${toString smp} \ - -cpu host \ - -machine type=q35 \ + QEMU_ARGS="-accel kvm -m ${toString memSize} -smp ${toString smp} -cpu host -machine type=q35 \ -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ -drive if=pflash,format=raw,file=vars.fd \ -rtc base=localtime,clock=host \ -device qemu-xhci -device usb-tablet \ -drive file=${resultImg},format=qcow2,if=virtio \ ${cdromArgs} \ - -nic user,model=virtio-net-pci + -nic user,model=virtio-net-pci" + + timeout 1800 qemu-system-x86_64 $VMIX_DISPLAY $QEMU_ARGS || \ + if [[ "$VMIX_DISPLAY" == "-display sdl" ]]; then + echo "=== vmix: SDL failed, retrying headless ===" + cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd && chmod +w vars.fd + timeout 1800 qemu-system-x86_64 -nographic $QEMU_ARGS + else + exit 1 + fi echo "=== vmix: audit script ${name} complete ===" ''; diff --git a/lib/images/windows/helpers/makeImage.nix b/lib/images/windows/helpers/makeImage.nix index 5cc1ac6..d48100b 100644 --- a/lib/images/windows/helpers/makeImage.nix +++ b/lib/images/windows/helpers/makeImage.nix @@ -53,19 +53,14 @@ let export DISPLAY=$(cat "$VMIX_DF") export HOME=$(mktemp -d) export XDG_RUNTIME_DIR=$HOME + export SDL_VIDEODRIVER=x11 VMIX_DISPLAY="-display sdl" fi ''} # Windows installs unattended, reboots into Audit Mode, # deletes cached Autounattend, shuts down → QEMU exits. - timeout 3600 qemu-system-x86_64 \ - $VMIX_DISPLAY \ - -accel kvm \ - -m ${toString memSize} \ - -smp ${toString smp} \ - -cpu host \ - -machine type=q35 \ + QEMU_ARGS="-accel kvm -m ${toString memSize} -smp ${toString smp} -cpu host -machine type=q35 \ -drive if=pflash,format=raw,readonly=on,file=${pkgs.OVMF.fd}/FV/OVMF_CODE.fd \ -drive if=pflash,format=raw,file=vars.fd \ -rtc base=localtime,clock=host \ @@ -73,7 +68,16 @@ let -drive file=disk.qcow2,format=qcow2,if=virtio \ -drive file=${iso},media=cdrom,readonly=on \ -drive file=${drivers.virtio-iso},media=cdrom,readonly=on \ - -nic user,model=virtio-net-pci + -nic user,model=virtio-net-pci" + + timeout 3600 qemu-system-x86_64 $VMIX_DISPLAY $QEMU_ARGS || \ + if [[ "$VMIX_DISPLAY" == "-display sdl" ]]; then + echo "=== vmix: SDL failed, retrying headless ===" + cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd && chmod +w vars.fd + timeout 3600 qemu-system-x86_64 -nographic $QEMU_ARGS + else + exit 1 + fi echo "=== vmix: ${name} install complete ===" mv disk.qcow2 $out diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index 807e1c4..29ef41f 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -53,6 +53,12 @@ in reg add "HKCU\Control Panel\Desktop" /v WallpaperStyle /t REG_SZ /d "0" /f ''} + :: Kill sysprep if it was triggered via CopyProfile'd startup entries + taskkill /f /im sysprep.exe 2>nul + :: Clean any leftover RunOnce/Run entries from audit phase + reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v "vmixAudit" /f 2>nul + reg delete "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v "vmixAudit" /f 2>nul + :: Remove Edge AppxPackage for current user (runs in user context during OOBE) :: The app is already removed on one of the templates but a ghost appx entry remains that can only be deleted at the user level powershell -Command "Get-AppxPackage *MicrosoftEdge* | Remove-AppxPackage -ErrorAction SilentlyContinue" @@ -148,7 +154,7 @@ in ''; in { - name = if delayOobeRun then "sealed" else "generalize"; + name = if delayOobeRun then "generalize-delay-oobe" else "generalize"; uploads = [ { source = oobeXml; dest = "/oobe-unattend.xml"; } { source = postOobeScript; dest = "/post-oobe.cmd"; } @@ -158,6 +164,10 @@ in { # generalize: sysprep + reboot into OOBE in the same QEMU session auditScript = '' @echo off + :: Remove cached Autounattend from initial install (contains Audit Mode reseal) + del /q C:\Windows\Panther\unattend.xml 2>nul + del /q C:\Windows\Panther\Unattend\unattend.xml 2>nul + del /q C:\Windows\System32\Sysprep\Panther\unattend.xml 2>nul C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe ${if delayOobeRun then "/shutdown" else "/reboot"} /quiet /unattend:C:\oobe-unattend.xml ''; }