vmix.nix/flake.nix
Git Sagar ebfb10b3b3 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>
2026-05-23 23:06:28 -03:00

288 lines
10 KiB
Nix

{
description = "vmix composable QEMU VM image building and orchestration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
};
outputs = { self, nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
lib = pkgs.lib;
vmixLib = import ./lib { inherit pkgs lib system; };
in {
overlays.default = import ./overlay.nix;
nixosModules.default = import ./module.nix;
lib.${system} = vmixLib;
packages.${system}.default = pkgs.writeShellScriptBin "vmix" ''
set -euo pipefail
usage() {
echo "Usage:"
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] [--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 " 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 " --out-link PATH Symlink for the build result (default: ./result)"
echo ""
echo "Examples:"
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 ""
echo " vmix copy --image windows.images.win10.laptop \\"
echo " --generalize username=Sagar,password=secret,hostname=LAPTOP \\"
echo " --to-remote-disk root@10.10.10.100:/dev/nvme0n1"
exit 1
}
[[ ''${#} -eq 0 ]] && usage
COMMAND="$1"; shift
case "$COMMAND" in
build|copy|run) ;;
--help|-h) usage ;;
*) echo "Unknown command: $COMMAND"; usage ;;
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=""
GENERALIZE=""
TO_DISK=""
TO_REMOTE_DISK=""
OUT_LINK="./result"
while [[ ''${#} -gt 0 ]]; do
case "$1" in
--image) IMAGE_NAME="$2"; shift 2 ;;
--generalize) GENERALIZE="$2"; shift 2 ;;
--to-disk) TO_DISK="$2"; shift 2 ;;
--to-remote-disk) TO_REMOTE_DISK="$2"; shift 2 ;;
--out-link) OUT_LINK="$2"; shift 2 ;;
--help|-h) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$IMAGE_NAME" ]] && { echo "Error: --image is required"; usage; }
if [[ "$COMMAND" == "copy" && -z "$TO_DISK" && -z "$TO_REMOTE_DISK" ]]; then
echo "Error: copy requires --to-disk or --to-remote-disk"
usage
fi
# parse --generalize key=val,key=val into nix attrs
GENERALIZE_EXPR=""
if [[ -n "$GENERALIZE" ]]; then
NIX_ARGS=""
IFS=',' read -ra PAIRS <<< "$GENERALIZE"
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
done
# CLI delay-oobe-run nix delayOobeRun
NIX_ARGS="''${NIX_ARGS//delay-oobe-run/delayOobeRun}"
GENERALIZE_EXPR=".generalize { $NIX_ARGS }"
fi
FLAKE_DIR="${self}"
echo "=== vmix $COMMAND ==="
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"
echo ""
# pass DISPLAY to nix builds via temp file (daemon sanitizes env vars)
VMIX_DISPLAY_FILE="/tmp/.vmix-display-$$"
if [[ -n "''${DISPLAY:-}" ]]; then
echo "$DISPLAY" > "$VMIX_DISPLAY_FILE"
chmod 666 "$VMIX_DISPLAY_FILE"
fi
trap 'rm -f "$VMIX_DISPLAY_FILE"' EXIT
echo "Building image..."
${pkgs.nix}/bin/nix build --out-link "$OUT_LINK" --print-build-logs --impure --expr "
let
vmixLib = (builtins.getFlake \"$FLAKE_DIR\").lib.${system};
image = vmixLib.$IMAGE_NAME;
in image$GENERALIZE_EXPR
"
IMAGE_FILE=$(readlink -f "$OUT_LINK")
echo "Built: $IMAGE_FILE"
if [[ -n "$TO_DISK" ]]; then
echo ""
[[ $EUID -ne 0 ]] && { echo "Error: --to-disk requires root"; exit 1; }
[[ ! -b "$TO_DISK" ]] && { echo "Error: not a block device: $TO_DISK"; exit 1; }
if ${pkgs.util-linux}/bin/mount | grep -q "^''${TO_DISK}"; then
echo "Error: $TO_DISK has mounted partitions unmount first"
exit 1
fi
DISK_SIZE=$(${pkgs.util-linux}/bin/blockdev --getsize64 "$TO_DISK")
DISK_SIZE_GB=$(( DISK_SIZE / 1024 / 1024 / 1024 ))
echo "=== writing to disk ==="
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; }
echo ""
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 "[2/4] Fixing GPT backup header..."
${pkgs.gptfdisk}/bin/sgdisk -e "$TO_DISK"
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
WIN_PART="''${TO_DISK}p3"
else
WIN_PART="''${TO_DISK}3"
fi
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"
echo ""
echo "Done. $TO_DISK is ready to boot."
fi
if [[ -n "$TO_REMOTE_DISK" ]]; then
echo ""
REMOTE_HOST="''${TO_REMOTE_DISK%%:*}"
REMOTE_DISK="''${TO_REMOTE_DISK#*:}"
[[ -z "$REMOTE_HOST" || -z "$REMOTE_DISK" ]] && { echo "Error: --to-remote-disk format is user@host:/dev/sdX"; exit 1; }
echo "=== writing to remote disk ==="
echo "Host: $REMOTE_HOST"
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; }
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 "[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"
else
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 ""
echo "Done. $REMOTE_HOST:$REMOTE_DISK is ready to boot."
fi
'';
apps.${system}.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/vmix";
};
};
}