diff --git a/flake.nix b/flake.nix index 30da4b5..2dbd6de 100644 --- a/flake.nix +++ b/flake.nix @@ -29,19 +29,16 @@ 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] [--snapshot]" 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 "" echo "Options:" 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 " --generalize KEY=VAL,... Generalize with comma-separated options:" + echo " username=User password= hostname=WIN-PC" 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" @@ -66,67 +63,15 @@ COMMAND="$1"; shift case "$COMMAND" in - build|copy|run) ;; + build|copy) ;; --help|-h) usage ;; *) echo "Unknown command: $COMMAND"; usage ;; esac - # --- run command --- - if [[ "$COMMAND" == "run" ]]; then - [[ ''${#} -lt 1 ]] && { echo "Error: vmix run [--mem 4096] [--smp 4]"; exit 1; } - RUN_INPUT="$1"; shift - RUN_MEM=4096 - RUN_SMP=4 - while [[ ''${#} -gt 0 ]]; do - case "$1" in - --mem) RUN_MEM="$2"; shift 2 ;; - --smp) RUN_SMP="$2"; shift 2 ;; - *) echo "Unknown option: $1"; exit 1 ;; - esac - done - - RUN_IMAGE="$RUN_INPUT" - [[ ! -f "$RUN_IMAGE" ]] && { echo "Error: file not found: $RUN_IMAGE"; exit 1; } - - VMIX_DISPLAY="-nographic" - if [[ -n "''${DISPLAY:-}" ]]; then - VMIX_DISPLAY="-display sdl" - fi - - cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd /tmp/vmix-run-vars-$$.fd - chmod +w /tmp/vmix-run-vars-$$.fd - trap 'rm -f /tmp/vmix-run-vars-$$.fd' EXIT - - echo "=== vmix run ===" - echo "Image: $RUN_IMAGE" - echo "Memory: $RUN_MEM MB" - echo "CPUs: $RUN_SMP" - echo "Display: $VMIX_DISPLAY" - echo "" - - exec ${pkgs.qemu}/bin/qemu-system-x86_64 \ - $VMIX_DISPLAY \ - -accel kvm \ - -m "$RUN_MEM" \ - -smp "$RUN_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=/tmp/vmix-run-vars-$$.fd \ - -rtc base=localtime,clock=host \ - -device qemu-xhci -device usb-tablet \ - -drive file="$RUN_IMAGE",format=qcow2,if=virtio,snapshot=on \ - -nic user,model=virtio-net-pci \ - -device virtio-serial-pci \ - -chardev spicevmc,id=vdagent,debug=0,name=vdagent \ - -device virtserialport,chardev=vdagent,name=com.redhat.spice.0 - fi - IMAGE_NAME="" GENERALIZE="" TO_DISK="" TO_REMOTE_DISK="" - YES=false OUT_LINK="./result" while [[ ''${#} -gt 0 ]]; do @@ -135,7 +80,6 @@ --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 ;; @@ -157,14 +101,8 @@ for pair in "''${PAIRS[@]}"; do key="''${pair%%=*}" val="''${pair#*=}" - if [[ "$val" == "true" || "$val" == "false" ]]; then - NIX_ARGS+="$key = $val; " - else - NIX_ARGS+="$key = \"$val\"; " - fi + NIX_ARGS+="$key = \"$val\"; " done - # CLI delay-oobe-run → nix delayOobeRun - NIX_ARGS="''${NIX_ARGS//delay-oobe-run/delayOobeRun}" GENERALIZE_EXPR=".generalize { $NIX_ARGS }" fi @@ -214,24 +152,17 @@ echo "Target: $TO_DISK ($DISK_SIZE_GB GB)" echo "" echo "WARNING: This will DESTROY all data on $TO_DISK" - if [[ "$YES" != "true" ]]; then - read -rp "Continue? [y/N] " confirm - [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } - fi + read -rp "Continue? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } echo "" - echo "[1/5] Wiping partition table..." - ${pkgs.gptfdisk}/bin/sgdisk --zap-all "$TO_DISK" - - echo "[2/5] Writing image to disk..." + echo "[1/4] Writing image to disk..." ${pkgs.qemu}/bin/qemu-img convert -p -S 4k -f qcow2 -O raw "$IMAGE_FILE" "$TO_DISK" - echo "[3/5] Fixing GPT backup header..." + echo "[2/4] 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 "[4/5] Expanding Windows partition (partition 3)..." + echo "[3/4] Expanding Windows partition (partition 3) to fill disk..." ${pkgs.parted}/bin/parted -s "$TO_DISK" resizepart 3 100% if [[ "$TO_DISK" == *nvme* ]] || [[ "$TO_DISK" == *mmcblk* ]]; then @@ -240,7 +171,7 @@ WIN_PART="''${TO_DISK}3" fi - echo "[5/5] Expanding NTFS filesystem on $WIN_PART..." + echo "[4/4] Expanding NTFS filesystem on $WIN_PART..." ${pkgs.ntfs3g}/bin/ntfsresize --force --no-action "$WIN_PART" echo "y" | ${pkgs.ntfs3g}/bin/ntfsresize --force "$WIN_PART" @@ -261,28 +192,19 @@ echo "Disk: $REMOTE_DISK" echo "" echo "WARNING: This will DESTROY all data on $REMOTE_HOST:$REMOTE_DISK" - if [[ "$YES" != "true" ]]; then - read -rp "Continue? [y/N] " confirm - [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } - fi + read -rp "Continue? [y/N] " confirm + [[ "$confirm" != "y" && "$confirm" != "Y" ]] && { echo "Aborted."; exit 0; } echo "" - echo "[1/5] Wiping partition table..." - ssh "$REMOTE_HOST" "sgdisk --zap-all $REMOTE_DISK" + 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 "[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" + 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%" if [[ "$REMOTE_DISK" == *nvme* ]] || [[ "$REMOTE_DISK" == *mmcblk* ]]; then REMOTE_WIN_PART="''${REMOTE_DISK}p3" @@ -290,14 +212,8 @@ 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)..." - 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 "[4/4] Expanding NTFS filesystem on $REMOTE_WIN_PART..." + ssh "$REMOTE_HOST" "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 9f4f4a4..08b8a25 100644 --- a/lib/images/windows/helpers/customizeImage.nix +++ b/lib/images/windows/helpers/customizeImage.nix @@ -80,36 +80,31 @@ chmod +w vars.fd VMIX_DISPLAY="-nographic" - ${lib.optionalString (vncDisplay != null) ''VMIX_DISPLAY="-vnc ${vncDisplay}"''} - ${lib.optionalString (vncDisplay == null) '' + ${lib.optionalString (displayArg != null) ''VMIX_DISPLAY="${displayArg}"''} + ${lib.optionalString (displayArg == 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} ===" - QEMU_ARGS="-accel kvm -m ${toString memSize} -smp ${toString smp} -cpu host -machine type=q35 \ + timeout 1800 qemu-system-x86_64 \ + $VMIX_DISPLAY \ + -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 \ + -global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \ -drive file=${resultImg},format=qcow2,if=virtio \ ${cdromArgs} \ - -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 + -nic user,model=virtio-net-pci echo "=== vmix: audit script ${name} complete ===" ''; diff --git a/lib/images/windows/helpers/makeImage.nix b/lib/images/windows/helpers/makeImage.nix index d48100b..67084b1 100644 --- a/lib/images/windows/helpers/makeImage.nix +++ b/lib/images/windows/helpers/makeImage.nix @@ -51,33 +51,28 @@ let 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 ''} # Windows installs unattended, reboots into Audit Mode, # deletes cached Autounattend, shuts down → QEMU exits. - QEMU_ARGS="-accel kvm -m ${toString memSize} -smp ${toString smp} -cpu host -machine type=q35 \ + timeout 3600 qemu-system-x86_64 \ + $VMIX_DISPLAY \ + -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 \ + -global ICH9-LMB.disable_s3=1 -global ICH9-LMB.disable_s4=1 \ -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" - - 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 + -nic user,model=virtio-net-pci echo "=== vmix: ${name} install complete ===" mv disk.qcow2 $out diff --git a/lib/images/windows/templates/default.nix b/lib/images/windows/templates/default.nix index 07ea447..c13e39c 100644 --- a/lib/images/windows/templates/default.nix +++ b/lib/images/windows/templates/default.nix @@ -19,7 +19,6 @@ in rec { vcppRuntimes = import ./essentials/vcpp-runtimes.nix args; bestPerformance = import ./essentials/best-performance.nix args; clearFileAssociations = import ./essentials/clear-file-associations.nix args; - virtioDrivers = import ./essentials/virtio-drivers.nix args; }; # Applications @@ -36,7 +35,7 @@ in rec { # Default file associations policy defaultApps = import ./default-apps.nix args; - # Generalize (sysprep + OOBE). Pass seal=true for hardware deployment. + # Generalize (sysprep + OOBE) generalize = import ./generalize.nix args; # Offline registry templates @@ -60,10 +59,10 @@ in rec { reg.performanceTweaks apps.edgeWebview apps.thorium + apps.sandboxie apps.sevenZip apps.vlc apps.imageGlass - essentials.virtioDrivers # needed for network during Office download apps.office ]; }; diff --git a/lib/images/windows/templates/essentials/virtio-drivers.nix b/lib/images/windows/templates/essentials/virtio-drivers.nix deleted file mode 100644 index dbc9fe5..0000000 --- a/lib/images/windows/templates/essentials/virtio-drivers.nix +++ /dev/null @@ -1,18 +0,0 @@ -# Install VirtIO drivers only (no guest agent or SPICE) -# Used during build for network access, not needed on real hardware -{ drivers, ... }: -{ - name = "virtio-drv"; - cdroms = [ drivers.virtio-iso ]; - auditScript = '' - @echo off - if exist D:\cert\virtio_win_cert.cer ( - certutil -addstore TrustedPublisher D:\cert\virtio_win_cert.cer - ) - :: Install drivers via pnputil (network, storage, balloon, serial) - for %%d in (NetKVM vioinput viostor vioscsi Balloon vioserial) do ( - if exist "D:\%%d\w10\amd64" pnputil /add-driver "D:\%%d\w10\amd64\*.inf" /install 2>nul - if exist "D:\%%d\w11\amd64" pnputil /add-driver "D:\%%d\w11\amd64\*.inf" /install 2>nul - ) - ''; -} diff --git a/lib/images/windows/templates/generalize.nix b/lib/images/windows/templates/generalize.nix index 29ef41f..efe1cc8 100644 --- a/lib/images/windows/templates/generalize.nix +++ b/lib/images/windows/templates/generalize.nix @@ -19,9 +19,6 @@ in timezone ? "UTC", # Desktop background solid color as hex string (e.g. "8e8cd8") bgColor ? null, - # delayOobeRun = true: sysprep only, OOBE + activation on real hardware - # delayOobeRun = false: sysprep + OOBE + activation in build VM - delayOobeRun ? false, }: let # Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry hexToRgbStr = hex: let @@ -53,12 +50,6 @@ 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" @@ -83,7 +74,7 @@ in del /q C:\vmix-audit-script.cmd 2>nul del /q C:\vmix-audit-wrapper.cmd 2>nul - ${if delayOobeRun then "" else "shutdown /s /t 5 /c \"vmix generalize complete\""} + shutdown /s /t 5 /c "vmix generalize complete" del /q C:\post-oobe.cmd 2>nul ''; @@ -154,21 +145,16 @@ in ''; in { - name = if delayOobeRun then "generalize-delay-oobe" else "generalize"; + name = "generalize"; uploads = [ { source = oobeXml; dest = "/oobe-unattend.xml"; } { source = postOobeScript; dest = "/post-oobe.cmd"; } { source = masScript; dest = "/MAS_AIO.cmd"; } ]; - # delayOobeRun: sysprep + shutdown — OOBE runs on real hardware - # generalize: sysprep + reboot into OOBE in the same QEMU session + # Sysprep reboots into OOBE within 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 + C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /reboot /quiet /unattend:C:\oobe-unattend.xml ''; }