vmix CLI, laptop images, SDL display
CLI (nix run .#): - `vmix build` and `vmix copy` subcommands - --image, --generalize key=val, --to-disk, --to-remote-disk - SDL display auto-detected via DISPLAY temp file passthrough - --print-build-logs for visible build progress - -S 4k sparse writes for faster disk copy Images: - win10.laptop and win11.laptop bundles (no VirtIO, keeps defender/hibernation) - templates.bundles.laptop shared template list - win11 adds reg.disableUCPD on top Build improvements: - consistent === vmix: === log prefixes - SDL display via /tmp/.vmix-display-$$ temp file Env helpers: - .env-export-vmix-cli-local: vmix alias for local flake Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
94f299bb81
commit
015714f713
8 changed files with 250 additions and 53 deletions
2
.env-export-vmix-cli-local
Normal file
2
.env-export-vmix-cli-local
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export VMIX_FLAKE="path:$PWD"
|
||||
alias vmix='nix run "$VMIX_FLAKE" --'
|
||||
12
flake.lock
generated
12
flake.lock
generated
|
|
@ -2,16 +2,16 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1715534503,
|
||||
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
|
||||
"owner": "nixos",
|
||||
"lastModified": 1779102034,
|
||||
"narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
|
||||
"rev": "687f05a9184cad4eaf905c48b63649e3a86f5433",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
|
|||
196
flake.nix
196
flake.nix
|
|
@ -21,61 +21,154 @@
|
|||
|
||||
lib.${system} = vmixLib;
|
||||
|
||||
packages.${system} = {
|
||||
toDisk = pkgs.writeShellScriptBin "vmix-to-disk" ''
|
||||
packages.${system}.default = pkgs.writeShellScriptBin "vmix" ''
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: vmix-to-disk <qcow2-image> <target-disk>"
|
||||
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 ""
|
||||
echo "Write a vmix qcow2 image to a physical disk and expand the Windows partition."
|
||||
echo "Commands:"
|
||||
echo " build Build a vmix image (optionally generalized)"
|
||||
echo " copy Build a vmix image and write it to a disk"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --image PATH Image path in vmixLib (e.g. windows.images.win10.laptop)"
|
||||
echo " --generalize KEY=VAL,... Generalize with comma-separated options:"
|
||||
echo " username=User password= hostname=WIN-PC"
|
||||
echo " timezone=UTC bgColor=8e8cd8"
|
||||
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-to-disk /nix/store/...-win10-ltsc-vmix.qcow2 /dev/sda"
|
||||
echo " vmix-to-disk ./my-image.qcow2 /dev/nvme0n1"
|
||||
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
|
||||
}
|
||||
|
||||
[[ ''${#} -ne 2 ]] && usage
|
||||
[[ ''${#} -eq 0 ]] && usage
|
||||
|
||||
IMAGE="$1"
|
||||
DISK="$2"
|
||||
COMMAND="$1"; shift
|
||||
|
||||
[[ $EUID -ne 0 ]] && { echo "Error: must run as root"; exit 1; }
|
||||
[[ ! -f "$IMAGE" ]] && { echo "Error: image not found: $IMAGE"; exit 1; }
|
||||
[[ ! -b "$DISK" ]] && { echo "Error: not a block device: $DISK"; exit 1; }
|
||||
case "$COMMAND" in
|
||||
build|copy) ;;
|
||||
--help|-h) usage ;;
|
||||
*) echo "Unknown command: $COMMAND"; usage ;;
|
||||
esac
|
||||
|
||||
if ${pkgs.util-linux}/bin/mount | grep -q "^''${DISK}"; then
|
||||
echo "Error: $DISK has mounted partitions — unmount first"
|
||||
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#*=}"
|
||||
NIX_ARGS+="$key = \"$val\"; "
|
||||
done
|
||||
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 "$DISK")
|
||||
DISK_SIZE=$(${pkgs.util-linux}/bin/blockdev --getsize64 "$TO_DISK")
|
||||
DISK_SIZE_GB=$(( DISK_SIZE / 1024 / 1024 / 1024 ))
|
||||
|
||||
echo "=== vmix to physical disk ==="
|
||||
echo "Image: $IMAGE"
|
||||
echo "Target: $DISK ($DISK_SIZE_GB GB)"
|
||||
echo "=== writing to disk ==="
|
||||
echo "Target: $TO_DISK ($DISK_SIZE_GB GB)"
|
||||
echo ""
|
||||
echo "WARNING: This will DESTROY all data on $DISK"
|
||||
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 -f qcow2 -O raw "$IMAGE" "$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 "$DISK"
|
||||
${pkgs.gptfdisk}/bin/sgdisk -e "$TO_DISK"
|
||||
|
||||
echo "[3/4] Expanding Windows partition (partition 3) to fill disk..."
|
||||
${pkgs.parted}/bin/parted -s "$DISK" resizepart 3 100%
|
||||
${pkgs.parted}/bin/parted -s "$TO_DISK" resizepart 3 100%
|
||||
|
||||
# detect partition device name (nvme/mmcblk use p3, others use 3)
|
||||
if [[ "$DISK" == *nvme* ]] || [[ "$DISK" == *mmcblk* ]]; then
|
||||
WIN_PART="''${DISK}p3"
|
||||
if [[ "$TO_DISK" == *nvme* ]] || [[ "$TO_DISK" == *mmcblk* ]]; then
|
||||
WIN_PART="''${TO_DISK}p3"
|
||||
else
|
||||
WIN_PART="''${DISK}3"
|
||||
WIN_PART="''${TO_DISK}3"
|
||||
fi
|
||||
|
||||
echo "[4/4] Expanding NTFS filesystem on $WIN_PART..."
|
||||
|
|
@ -83,14 +176,53 @@
|
|||
echo "y" | ${pkgs.ntfs3g}/bin/ntfsresize --force "$WIN_PART"
|
||||
|
||||
echo ""
|
||||
echo "Done. Windows partition expanded to fill $DISK."
|
||||
echo "You can now boot from the disk."
|
||||
'';
|
||||
};
|
||||
echo "Done. $TO_DISK is ready to boot."
|
||||
fi
|
||||
|
||||
apps.${system}.toDisk = {
|
||||
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}.toDisk}/bin/vmix-to-disk";
|
||||
program = "${self.packages.${system}.default}/bin/vmix";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@
|
|||
|
||||
cdromArgs = lib.concatMapStringsSep " \\\n " (cd: "-drive file=${cd},media=cdrom,readonly=on") cdroms;
|
||||
|
||||
displayArg = if vncDisplay != null then "-vnc ${vncDisplay}" else null;
|
||||
|
||||
auditBootCommands = lib.optionalString hasAuditScript ''
|
||||
|
||||
echo "=== vmix: injecting audit script (${name}) ==="
|
||||
|
|
@ -77,9 +79,19 @@
|
|||
cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd
|
||||
chmod +w vars.fd
|
||||
|
||||
echo "=== vmix: booting Audit Mode for ${name} (VNC: ${if vncDisplay != null then vncDisplay else "disabled"}) ==="
|
||||
VMIX_DISPLAY="-nographic"
|
||||
${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")
|
||||
VMIX_DISPLAY="-display sdl"
|
||||
fi
|
||||
''}
|
||||
|
||||
echo "=== vmix: booting Audit Mode for ${name} ==="
|
||||
timeout 1800 qemu-system-x86_64 \
|
||||
${if vncDisplay != null then "-vnc ${vncDisplay}" else "-nographic"} \
|
||||
$VMIX_DISPLAY \
|
||||
-accel kvm \
|
||||
-m ${toString memSize} \
|
||||
-smp ${toString smp} \
|
||||
|
|
|
|||
|
|
@ -30,24 +30,35 @@ let
|
|||
inherit AutounattendedXml;
|
||||
};
|
||||
|
||||
displayArg = if vncDisplay != null then "-vnc ${vncDisplay}" else null;
|
||||
|
||||
drv = pkgs.runCommand "${name}-vmix.qcow2" {
|
||||
__noChroot = true;
|
||||
requiredSystemFeatures = [ "kvm" ];
|
||||
nativeBuildInputs = with pkgs; [ qemu ];
|
||||
} ''
|
||||
# create empty disk
|
||||
echo "=== vmix: creating ${diskSize} disk ==="
|
||||
qemu-img create -f qcow2 disk.qcow2 ${diskSize}
|
||||
|
||||
# writable UEFI NVRAM so boot order persists across reboots
|
||||
cp ${pkgs.OVMF.fd}/FV/OVMF_VARS.fd vars.fd
|
||||
chmod +w vars.fd
|
||||
|
||||
echo "=== Starting Windows unattended install (this takes 15-30 minutes) ==="
|
||||
echo "=== vmix: installing ${name} (unattended, 5-10 min) ==="
|
||||
|
||||
VMIX_DISPLAY="-nographic"
|
||||
${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")
|
||||
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 \
|
||||
${if vncDisplay != null then "-vnc ${vncDisplay}" else "-nographic"} \
|
||||
$VMIX_DISPLAY \
|
||||
-accel kvm \
|
||||
-m ${toString memSize} \
|
||||
-smp ${toString smp} \
|
||||
|
|
@ -63,7 +74,7 @@ let
|
|||
-drive file=${drivers.virtio-iso},media=cdrom,readonly=on \
|
||||
-nic user,model=virtio-net-pci
|
||||
|
||||
echo "=== Windows install complete (Audit Mode image) ==="
|
||||
echo "=== vmix: ${name} install complete ==="
|
||||
mv disk.qcow2 $out
|
||||
'';
|
||||
in drv // { _vmixOsType = "windows"; }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
{ pkgs, lib, system, drivers, makeFilesISO, ... }:
|
||||
let
|
||||
args = { inherit pkgs lib system drivers makeFilesISO; };
|
||||
in {
|
||||
in rec {
|
||||
# Essentials (drivers, runtimes, removals, performance)
|
||||
essentials = {
|
||||
virtioTools = import ./essentials/virtio-tools.nix args;
|
||||
|
|
@ -40,4 +40,30 @@ in {
|
|||
|
||||
# Offline registry templates
|
||||
reg = import ./registry args;
|
||||
|
||||
# Bundles — reusable template lists for common use cases
|
||||
bundles = {
|
||||
laptop = [
|
||||
essentials.removeIE
|
||||
essentials.removeWMP
|
||||
essentials.removeEdge
|
||||
essentials.vcppRuntimes
|
||||
essentials.bestPerformance
|
||||
reg.disableTelemetry
|
||||
reg.disableErrorReporting
|
||||
reg.disableUpdates
|
||||
reg.disableSmartScreen
|
||||
reg.disablePrivacyTracking
|
||||
reg.disableAI
|
||||
reg.disableConsumerFeatures
|
||||
reg.performanceTweaks
|
||||
apps.edgeWebview
|
||||
apps.thorium
|
||||
apps.sandboxie
|
||||
apps.sevenZip
|
||||
apps.vlc
|
||||
apps.imageGlass
|
||||
apps.office
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,4 +31,10 @@ with windows;
|
|||
|
||||
withAMDGPU = customizeImage basic templates.essentials.amdGpuDrivers;
|
||||
};
|
||||
|
||||
laptop = customizeImageFold (makeImage {
|
||||
name = "win10-ltsc-2021-laptop";
|
||||
upstreamISO = upstreamISOs.win10-ltsc-2021;
|
||||
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
||||
}) templates.bundles.laptop;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,4 +35,12 @@ with windows;
|
|||
|
||||
withAMDGPU = customizeImage basic templates.essentials.amdGpuDrivers;
|
||||
};
|
||||
|
||||
laptop = customizeImageFold (makeImage {
|
||||
name = "win11-ltsc-2024-laptop";
|
||||
upstreamISO = upstreamISOs.win11-ltsc-2024;
|
||||
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
||||
bypassRequirements = true;
|
||||
windowsVersionForVirtioDrivers = "w11";
|
||||
}) (templates.bundles.laptop ++ [ templates.reg.disableUCPD ]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue