vmix run command, virtio-drivers template, delay-oobe-run flag

CLI:
- `vmix run <qcow2>` boots image with QEMU (SDL if DISPLAY, snapshot mode)
- --generalize supports delay-oobe-run=true to defer OOBE + activation
  to first boot on real hardware (for physical disk deployments)

Templates:
- essentials.virtioDrivers: installs VirtIO drivers only (no guest agent)
  used in laptop bundle for network access during Office download
- generalize: delayOobeRun flag controls sysprep /shutdown vs /reboot
  delays OOBE, user creation and HWID activation to target device

Build:
- suppress XDG_RUNTIME_DIR and homeless-shelter warnings in SDL mode
- remove invalid ICH9-LMB global properties

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Git Sagar 2026-05-23 23:06:28 -03:00
parent 015714f713
commit ebfb10b3b3
6 changed files with 97 additions and 12 deletions

View file

@ -29,16 +29,19 @@
echo " vmix build --image <path> [--generalize key=val,...] [--out-link PATH]" 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-disk /dev/sdX"
echo " vmix copy --image <path> [--generalize key=val,...] --to-remote-disk user@host:/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] [--snapshot]"
echo "" echo ""
echo "Commands:" echo "Commands:"
echo " build Build a vmix image (optionally generalized)" echo " build Build a vmix image (optionally generalized)"
echo " copy Build a vmix image and write it to a disk" 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 ""
echo "Options:" 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 " --generalize KEY=VAL,... Generalize with comma-separated options:" echo " --generalize KEY=VAL,... Finalize image with comma-separated options:"
echo " username=User password= hostname=WIN-PC" echo " username=User password= hostname=PC"
echo " timezone=UTC bgColor=8e8cd8" 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-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 " --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 " e.g. root@10.10.10.100:/dev/sda"
@ -63,11 +66,62 @@
COMMAND="$1"; shift COMMAND="$1"; shift
case "$COMMAND" in case "$COMMAND" in
build|copy) ;; build|copy|run) ;;
--help|-h) usage ;; --help|-h) usage ;;
*) echo "Unknown command: $COMMAND"; usage ;; *) echo "Unknown command: $COMMAND"; usage ;;
esac esac
# --- run command ---
if [[ "$COMMAND" == "run" ]]; then
[[ ''${#} -lt 1 ]] && { echo "Error: vmix run <qcow2-file-or-image-name> [--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="" IMAGE_NAME=""
GENERALIZE="" GENERALIZE=""
TO_DISK="" TO_DISK=""
@ -101,8 +155,14 @@
for pair in "''${PAIRS[@]}"; do for pair in "''${PAIRS[@]}"; do
key="''${pair%%=*}" key="''${pair%%=*}"
val="''${pair#*=}" val="''${pair#*=}"
NIX_ARGS+="$key = \"$val\"; " if [[ "$val" == "true" || "$val" == "false" ]]; then
NIX_ARGS+="$key = $val; "
else
NIX_ARGS+="$key = \"$val\"; "
fi
done done
# CLI delay-oobe-run → nix delayOobeRun
NIX_ARGS="''${NIX_ARGS//delay-oobe-run/delayOobeRun}"
GENERALIZE_EXPR=".generalize { $NIX_ARGS }" GENERALIZE_EXPR=".generalize { $NIX_ARGS }"
fi fi

View file

@ -85,6 +85,8 @@
VMIX_DF=$(ls -t /tmp/.vmix-display-* 2>/dev/null | head -1) VMIX_DF=$(ls -t /tmp/.vmix-display-* 2>/dev/null | head -1)
if [ -n "$VMIX_DF" ]; then if [ -n "$VMIX_DF" ]; then
export DISPLAY=$(cat "$VMIX_DF") export DISPLAY=$(cat "$VMIX_DF")
export HOME=$(mktemp -d)
export XDG_RUNTIME_DIR=$HOME
VMIX_DISPLAY="-display sdl" VMIX_DISPLAY="-display sdl"
fi fi
''} ''}
@ -101,7 +103,6 @@
-drive if=pflash,format=raw,file=vars.fd \ -drive if=pflash,format=raw,file=vars.fd \
-rtc base=localtime,clock=host \ -rtc base=localtime,clock=host \
-device qemu-xhci -device usb-tablet \ -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 \ -drive file=${resultImg},format=qcow2,if=virtio \
${cdromArgs} \ ${cdromArgs} \
-nic user,model=virtio-net-pci -nic user,model=virtio-net-pci

View file

@ -51,6 +51,8 @@ let
VMIX_DF=$(ls -t /tmp/.vmix-display-* 2>/dev/null | head -1) VMIX_DF=$(ls -t /tmp/.vmix-display-* 2>/dev/null | head -1)
if [ -n "$VMIX_DF" ]; then if [ -n "$VMIX_DF" ]; then
export DISPLAY=$(cat "$VMIX_DF") export DISPLAY=$(cat "$VMIX_DF")
export HOME=$(mktemp -d)
export XDG_RUNTIME_DIR=$HOME
VMIX_DISPLAY="-display sdl" VMIX_DISPLAY="-display sdl"
fi fi
''} ''}
@ -68,7 +70,6 @@ let
-drive if=pflash,format=raw,file=vars.fd \ -drive if=pflash,format=raw,file=vars.fd \
-rtc base=localtime,clock=host \ -rtc base=localtime,clock=host \
-device qemu-xhci -device usb-tablet \ -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=disk.qcow2,format=qcow2,if=virtio \
-drive file=${iso},media=cdrom,readonly=on \ -drive file=${iso},media=cdrom,readonly=on \
-drive file=${drivers.virtio-iso},media=cdrom,readonly=on \ -drive file=${drivers.virtio-iso},media=cdrom,readonly=on \

View file

@ -19,6 +19,7 @@ in rec {
vcppRuntimes = import ./essentials/vcpp-runtimes.nix args; vcppRuntimes = import ./essentials/vcpp-runtimes.nix args;
bestPerformance = import ./essentials/best-performance.nix args; bestPerformance = import ./essentials/best-performance.nix args;
clearFileAssociations = import ./essentials/clear-file-associations.nix args; clearFileAssociations = import ./essentials/clear-file-associations.nix args;
virtioDrivers = import ./essentials/virtio-drivers.nix args;
}; };
# Applications # Applications
@ -35,7 +36,7 @@ in rec {
# Default file associations policy # Default file associations policy
defaultApps = import ./default-apps.nix args; defaultApps = import ./default-apps.nix args;
# Generalize (sysprep + OOBE) # Generalize (sysprep + OOBE). Pass seal=true for hardware deployment.
generalize = import ./generalize.nix args; generalize = import ./generalize.nix args;
# Offline registry templates # Offline registry templates
@ -59,10 +60,10 @@ in rec {
reg.performanceTweaks reg.performanceTweaks
apps.edgeWebview apps.edgeWebview
apps.thorium apps.thorium
apps.sandboxie
apps.sevenZip apps.sevenZip
apps.vlc apps.vlc
apps.imageGlass apps.imageGlass
essentials.virtioDrivers # needed for network during Office download
apps.office apps.office
]; ];
}; };

View file

@ -0,0 +1,18 @@
# 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
)
'';
}

View file

@ -19,6 +19,9 @@ in
timezone ? "UTC", timezone ? "UTC",
# Desktop background solid color as hex string (e.g. "8e8cd8") # Desktop background solid color as hex string (e.g. "8e8cd8")
bgColor ? null, bgColor ? null,
# delayOobeRun = true: sysprep only, OOBE + activation on real hardware
# delayOobeRun = false: sysprep + OOBE + activation in build VM
delayOobeRun ? false,
}: let }: let
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry # Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
hexToRgbStr = hex: let hexToRgbStr = hex: let
@ -74,7 +77,7 @@ in
del /q C:\vmix-audit-script.cmd 2>nul del /q C:\vmix-audit-script.cmd 2>nul
del /q C:\vmix-audit-wrapper.cmd 2>nul del /q C:\vmix-audit-wrapper.cmd 2>nul
shutdown /s /t 5 /c "vmix generalize complete" ${if delayOobeRun then "" else "shutdown /s /t 5 /c \"vmix generalize complete\""}
del /q C:\post-oobe.cmd 2>nul del /q C:\post-oobe.cmd 2>nul
''; '';
@ -145,16 +148,17 @@ in
</unattend> </unattend>
''; '';
in { in {
name = "generalize"; name = if delayOobeRun then "sealed" else "generalize";
uploads = [ uploads = [
{ source = oobeXml; dest = "/oobe-unattend.xml"; } { source = oobeXml; dest = "/oobe-unattend.xml"; }
{ source = postOobeScript; dest = "/post-oobe.cmd"; } { source = postOobeScript; dest = "/post-oobe.cmd"; }
{ source = masScript; dest = "/MAS_AIO.cmd"; } { source = masScript; dest = "/MAS_AIO.cmd"; }
]; ];
# Sysprep reboots into OOBE within the same QEMU session # delayOobeRun: sysprep + shutdown — OOBE runs on real hardware
# generalize: sysprep + reboot into OOBE in the same QEMU session
auditScript = '' auditScript = ''
@echo off @echo off
C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /reboot /quiet /unattend:C:\oobe-unattend.xml C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe ${if delayOobeRun then "/shutdown" else "/reboot"} /quiet /unattend:C:\oobe-unattend.xml
''; '';
} }