Compare commits
3 commits
master
...
wip/window
| Author | SHA1 | Date | |
|---|---|---|---|
| b6a080af4b | |||
| f33b8e7ce3 | |||
| 3b454b749a |
11 changed files with 718 additions and 15 deletions
|
|
@ -15,12 +15,9 @@
|
||||||
lib = pkgs.lib;
|
lib = pkgs.lib;
|
||||||
vmixLib = import ./lib { inherit pkgs lib system; };
|
vmixLib = import ./lib { inherit pkgs lib system; };
|
||||||
in {
|
in {
|
||||||
overlays.default = final: prev: { inherit vmixLib; };
|
overlays.default = import ./overlay.nix;
|
||||||
|
|
||||||
nixosModules.default = { config, pkgs, lib, ... }: {
|
nixosModules.default = import ./module.nix;
|
||||||
imports = [ ./nixos/default.nix ];
|
|
||||||
config.nixpkgs.overlays = [ self.overlays.default ];
|
|
||||||
};
|
|
||||||
|
|
||||||
lib.${system} = vmixLib;
|
lib.${system} = vmixLib;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ in rec {
|
||||||
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;
|
virtioDrivers = import ./essentials/virtio-drivers.nix args;
|
||||||
|
windowsUpdate = import ./essentials/windows-update.nix args;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Applications
|
# Applications
|
||||||
|
|
|
||||||
114
lib/images/windows/templates/essentials/windows-update.nix
Normal file
114
lib/images/windows/templates/essentials/windows-update.nix
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Apply all available Windows Updates via the Windows Update COM API in Audit Mode.
|
||||||
|
# Handles reboots automatically — re-registers via RunOnce and continues updating.
|
||||||
|
# Compacts the image afterward to flatten the COW chain.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# essentials.windowsUpdate {}
|
||||||
|
# essentials.windowsUpdate { maxRounds = 5; }
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
{ maxRounds ? 3 }:
|
||||||
|
{
|
||||||
|
name = "windows-update";
|
||||||
|
compact = true;
|
||||||
|
memSize = 4096;
|
||||||
|
qemuTimeout = 7200;
|
||||||
|
auditScript = ''
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
:: Track update round via a counter file
|
||||||
|
set "ROUND_FILE=C:\vmix-update-round.txt"
|
||||||
|
set "MAX_ROUNDS=${toString maxRounds}"
|
||||||
|
|
||||||
|
if exist "%ROUND_FILE%" (
|
||||||
|
set /p ROUND=<"%ROUND_FILE%"
|
||||||
|
) else (
|
||||||
|
set "ROUND=1"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === vmix: Windows Update round %ROUND% of %MAX_ROUNDS% ===
|
||||||
|
|
||||||
|
:: Ensure Windows Update service is running
|
||||||
|
net start wuauserv 2>nul
|
||||||
|
sc config wuauserv start= auto
|
||||||
|
|
||||||
|
:: Remove any update-blocking policies (LTSC may have these)
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||||
|
|
||||||
|
:: Give the service time to initialize on first round
|
||||||
|
if "%ROUND%"=="1" (
|
||||||
|
echo Waiting for Windows Update service to initialize...
|
||||||
|
timeout /t 30 /nobreak >nul
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Run Windows Update via PowerShell COM API
|
||||||
|
powershell -ExecutionPolicy Bypass -Command ^
|
||||||
|
"try {" ^
|
||||||
|
" $session = New-Object -ComObject Microsoft.Update.Session;" ^
|
||||||
|
" $searcher = $session.CreateUpdateSearcher();" ^
|
||||||
|
" Write-Host 'Searching for updates...';" ^
|
||||||
|
" $result = $searcher.Search('IsInstalled=0');" ^
|
||||||
|
" $count = $result.Updates.Count;" ^
|
||||||
|
" Write-Host \"Found $count updates\";" ^
|
||||||
|
" if ($count -eq 0) { exit 0 };" ^
|
||||||
|
" foreach ($u in $result.Updates) { Write-Host \" - $($u.Title)\" };" ^
|
||||||
|
" $updatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl;" ^
|
||||||
|
" foreach ($u in $result.Updates) {" ^
|
||||||
|
" if ($u.EulaAccepted -eq $false) { $u.AcceptEula() };" ^
|
||||||
|
" $updatesToInstall.Add($u) | Out-Null" ^
|
||||||
|
" };" ^
|
||||||
|
" $downloader = $session.CreateUpdateDownloader();" ^
|
||||||
|
" $downloader.Updates = $updatesToInstall;" ^
|
||||||
|
" Write-Host 'Downloading...';" ^
|
||||||
|
" $downloader.Download() | Out-Null;" ^
|
||||||
|
" $installer = $session.CreateUpdateInstaller();" ^
|
||||||
|
" $installer.Updates = $updatesToInstall;" ^
|
||||||
|
" Write-Host 'Installing...';" ^
|
||||||
|
" $installResult = $installer.Install();" ^
|
||||||
|
" Write-Host \"Result: $($installResult.ResultCode)\";" ^
|
||||||
|
" for ($i = 0; $i -lt $updatesToInstall.Count; $i++) {" ^
|
||||||
|
" $hr = $installResult.GetUpdateResult($i).HResult;" ^
|
||||||
|
" Write-Host \" $($updatesToInstall.Item($i).Title): code=$hr\"" ^
|
||||||
|
" };" ^
|
||||||
|
" if ($installResult.RebootRequired) { exit 3010 } else { exit 0 }" ^
|
||||||
|
"} catch {" ^
|
||||||
|
" Write-Host \"ERROR: $_\";" ^
|
||||||
|
" exit 1" ^
|
||||||
|
"}"
|
||||||
|
|
||||||
|
set "WU_EXIT=%ERRORLEVEL%"
|
||||||
|
echo Windows Update exit code: %WU_EXIT%
|
||||||
|
|
||||||
|
:: Cleanup component store
|
||||||
|
echo Cleaning up component store...
|
||||||
|
dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet 2>nul
|
||||||
|
|
||||||
|
:: Check if we need to reboot and continue
|
||||||
|
set /a "NEXT_ROUND=%ROUND%+1"
|
||||||
|
|
||||||
|
if "%WU_EXIT%"=="3010" (
|
||||||
|
if %ROUND% LSS %MAX_ROUNDS% (
|
||||||
|
echo Reboot required, scheduling round %NEXT_ROUND%...
|
||||||
|
echo %NEXT_ROUND% > "%ROUND_FILE%"
|
||||||
|
:: Copy script to a path the wrapper won't delete
|
||||||
|
copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
|
||||||
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" /v vmixUpdate /t REG_SZ /d "cmd /c C:\vmix-update-continue.cmd" /f
|
||||||
|
:: Preserve Audit Mode across reboot (updates can reset it)
|
||||||
|
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||||
|
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||||
|
:: Immediate reboot (preempts wrapper shutdown)
|
||||||
|
shutdown /r /f /t 0
|
||||||
|
exit /b
|
||||||
|
) else (
|
||||||
|
echo Max update rounds reached.
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Done — clean up and shutdown
|
||||||
|
del /q "%ROUND_FILE%" 2>nul
|
||||||
|
del /q "C:\vmix-update-continue.cmd" 2>nul
|
||||||
|
echo === vmix: Windows Update complete ===
|
||||||
|
shutdown /s /f /t 10 /c "vmix: windows-update complete"
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,9 @@ rec {
|
||||||
upstreamISO = upstreamISOs.win10-ltsc-2021;
|
upstreamISO = upstreamISOs.win10-ltsc-2021;
|
||||||
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
||||||
};
|
};
|
||||||
basic = customizeImageFold upstream (with templates; [
|
# Apply all available Windows Updates (cumulative, .NET, Defender)
|
||||||
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated (with templates; [
|
||||||
essentials.virtioTools
|
essentials.virtioTools
|
||||||
essentials.removeIE
|
essentials.removeIE
|
||||||
essentials.removeWMP
|
essentials.removeWMP
|
||||||
|
|
@ -38,8 +40,9 @@ rec {
|
||||||
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
productKey = "M7XTQ-FN8P6-TTKYV-9D4CC-J462D";
|
||||||
useAHCI = true;
|
useAHCI = true;
|
||||||
};
|
};
|
||||||
|
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
|
||||||
|
|
||||||
laptopSlim = customizeImageFold laptopUpstream templates.bundles.laptopSlim;
|
laptopSlim = customizeImageFold laptopUpdated templates.bundles.laptopSlim;
|
||||||
|
|
||||||
laptop = customizeImageFold laptopUpstream templates.bundles.laptop;
|
laptop = customizeImageFold laptopUpdated templates.bundles.laptop;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ rec {
|
||||||
windowsVersionForVirtioDrivers = "w11";
|
windowsVersionForVirtioDrivers = "w11";
|
||||||
};
|
};
|
||||||
|
|
||||||
basic = customizeImageFold upstream (with templates; [
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated (with templates; [
|
||||||
essentials.virtioTools
|
essentials.virtioTools
|
||||||
essentials.removeIE
|
essentials.removeIE
|
||||||
essentials.removeWMP
|
essentials.removeWMP
|
||||||
|
|
@ -45,9 +46,11 @@ rec {
|
||||||
windowsVersionForVirtioDrivers = "w11";
|
windowsVersionForVirtioDrivers = "w11";
|
||||||
};
|
};
|
||||||
|
|
||||||
laptopSlim = customizeImageFold laptopUpstream
|
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
|
||||||
|
|
||||||
|
laptopSlim = customizeImageFold laptopUpdated
|
||||||
(templates.bundles.laptopSlim ++ [ templates.reg.disableUCPD ]);
|
(templates.bundles.laptopSlim ++ [ templates.reg.disableUCPD ]);
|
||||||
|
|
||||||
laptop = customizeImageFold laptopUpstream
|
laptop = customizeImageFold laptopUpdated
|
||||||
(templates.bundles.laptop ++ [ templates.reg.disableUCPD ]);
|
(templates.bundles.laptop ++ [ templates.reg.disableUCPD ]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
module.nix
Normal file
6
module.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./nixos/default.nix
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
vmixLib = pkgs.vmixLib;
|
vmixLib = import ./../lib {inherit pkgs lib; };
|
||||||
args = { inherit config pkgs lib vmixLib; };
|
args = { inherit config pkgs lib vmixLib; };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -15,4 +15,6 @@ in
|
||||||
(types.submodule (import ./namespaceSubmoduleOptions.nix args));
|
(types.submodule (import ./namespaceSubmoduleOptions.nix args));
|
||||||
default = {};
|
default = {};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
config.nixpkgs.overlays = [ (import ../overlay.nix) ];
|
||||||
|
}
|
||||||
|
|
@ -286,6 +286,5 @@ in
|
||||||
{
|
{
|
||||||
config.systemd.services = namespaceGlobalService // networkServices;
|
config.systemd.services = namespaceGlobalService // networkServices;
|
||||||
config.systemd.targets = networkTargets;
|
config.systemd.targets = networkTargets;
|
||||||
config.boot.kernel.sysctl."net.ipv4.ip_forward" = lib.mkForce 1;
|
config.boot.kernel.sysctl."net.ipv4.ip_forward" = lib.mkDefault 1;
|
||||||
config.boot.kernel.sysctl."net.ipv4.conf.all.forwarding" = lib.mkForce true;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
overlay.nix
Normal file
7
overlay.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
final: prev:
|
||||||
|
let
|
||||||
|
# Pin vmixLib to nixpkgs 25-11 so all VM images are built with a consistent toolchain
|
||||||
|
vmixPkgs = prev.v25-11 or prev;
|
||||||
|
in {
|
||||||
|
vmixLib = vmixPkgs.callPackage ./lib {};
|
||||||
|
}
|
||||||
41
wip/claude-memory/MEMORY.md
Normal file
41
wip/claude-memory/MEMORY.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# vmix.nix Project Memory
|
||||||
|
|
||||||
|
## Build Preferences
|
||||||
|
- Always use VNC display (`:1` / port 5901) when building Windows images so progress can be monitored
|
||||||
|
- Pass `vncDisplay = ":1"` to customizeImage templates for build monitoring
|
||||||
|
- Use `gvnccapture localhost:1 /tmp/screenshot.png` to take VNC screenshots (package: gtk-vnc)
|
||||||
|
- For VNC inside vmix namespace: `ip netns exec windows.vmix nix-shell -p gtk-vnc --run 'gvnccapture 127.0.0.1:1 /tmp/screenshot.png'`
|
||||||
|
|
||||||
|
## Key Architecture
|
||||||
|
- Offline registry uses `ControlSet001` (not `CurrentControlSet`) for virt-win-reg merges
|
||||||
|
- Sysprep resets offline registry changes — RDP must be re-enabled in post-OOBE script
|
||||||
|
- TermService won't listen on port 3389 in Audit Mode without a password on Administrator
|
||||||
|
- `LimitBlankPasswordUse=0` alone is NOT sufficient for RDP in Audit Mode — password required
|
||||||
|
- OOBE AutoLogon `<Password>` in unattend XML is unreliable — set via `reg add` in post-oobe.cmd instead
|
||||||
|
- OOBE creates user with blank password regardless of unattend — set real password via `net user` in post-oobe.cmd
|
||||||
|
- `sc config` can fail silently for some services — use `reg add` to set `Start` value directly
|
||||||
|
|
||||||
|
## RDP on Win10 IoT Enterprise LTSC 2021
|
||||||
|
- **CRITICAL**: `rdpwd.sys` and `tdtcp.sys` don't exist in this Windows build (removed in 19041+)
|
||||||
|
- `termsrv.dll` version is `10.0.19041.1202` — not supported by RDPWrap v1.6.2 or community INI files
|
||||||
|
- TermService runs but never creates the `rdp-tcp` WinStation listener — no port 3389
|
||||||
|
- The ISO (`en-us_windows_10_iot_enterprise_ltsc_2021_x64_dvd_257ad90f.iso`) has 2 indexes:
|
||||||
|
- Index 1: Windows 10 Enterprise LTSC 2021
|
||||||
|
- Index 2: Windows 10 IoT Enterprise LTSC 2021
|
||||||
|
- Product key `M7XTQ-FN8P6-TTKYV-9D4CC-J462D` = Enterprise LTSC (not IoT)
|
||||||
|
- MAS HWID activation switches edition to IoT Enterprise S (partial key YY74H)
|
||||||
|
- **TODO**: Either generate custom RDPWrap config for termsrv 10.0.19041.1202, use a different ISO, or use third-party RDP server
|
||||||
|
|
||||||
|
## generalize.nix Changes
|
||||||
|
- `enableRDP` flag added — applies RDP settings in post-oobe.cmd (survives sysprep)
|
||||||
|
- AutoLogon fix: blank password in unattend, real password + AutoAdminLogon registry in post-oobe.cmd
|
||||||
|
- `LogonCount=999` for persistent AutoLogon
|
||||||
|
- SessionEnv + UmRdpService set to auto-start via `reg add` (Start=2)
|
||||||
|
- Firewall: `Enable-NetFirewallRule -DisplayGroup 'Remote Desktop'` + `Set-NetFirewallRule -Profile Any`
|
||||||
|
|
||||||
|
## labv2.nix junto Deployment
|
||||||
|
- vmix flake input rev is pinned in `flake.nix` — must update the URL to change versions
|
||||||
|
- Use `path:/storage/gitrepos/vmix.nix` for local dev, `git+https://...?rev=<hash>` for production
|
||||||
|
- `colmena apply-local` doesn't support `--override-input`
|
||||||
|
- DNS: `dns.resolver.useHostResolvConf = true` breaks when host uses systemd-resolved (127.0.0.53) — use explicit upstream like `1.1.1.1`
|
||||||
|
- QEMU Guest Agent socket at `/tmp/qga-win10.sock` — use from inside namespace
|
||||||
530
wip/win10-update.session.md
Normal file
530
wip/win10-update.session.md
Normal file
|
|
@ -0,0 +1,530 @@
|
||||||
|
# Windows Update Template - Full Development Session Log
|
||||||
|
|
||||||
|
## Original Request
|
||||||
|
User wanted a reproducible way to update Windows images, inspired by https://massgrave.dev/update-windows-iso. The upstream Win10 LTSC 2021 ISO is stored in a git-lfs repo at `git.sagar.ch`.
|
||||||
|
|
||||||
|
## Approach Evolution
|
||||||
|
|
||||||
|
### Phase 1: ISO Update Approach (abandoned)
|
||||||
|
Initially explored creating an updated ISO by integrating cumulative updates. Considered:
|
||||||
|
|
||||||
|
1. **UUP Dump approach**: The massgrave page links to uupdump.net which provides download scripts for building fresh ISOs from Microsoft's UUP (Unified Update Platform) files. The user received a bash script from UUP dump that:
|
||||||
|
- Uses `aria2c` to download UUP .cab files from Microsoft CDN
|
||||||
|
- Uses `cabextract`, `wimlib-imagex`, `chntpw` to build an ISO
|
||||||
|
- Uses `genisoimage`/`mkisofs` to create the final ISO
|
||||||
|
- Problem: downloads `core;professional` (Home/Pro), NOT LTSC
|
||||||
|
- Problem: download URLs contain auth tokens that expire — not reproducible
|
||||||
|
|
||||||
|
2. **Offline DISM approach**: Considered creating a Nix derivation that:
|
||||||
|
- Boots a Windows worker VM from the existing upstream image
|
||||||
|
- Mounts the original ISO inside the VM
|
||||||
|
- Uses `DISM /Image` to service install.wim offline (mount WIM, apply .cab updates, unmount)
|
||||||
|
- Extracts the updated install.wim via guestfs
|
||||||
|
- Rebuilds the ISO with genisoimage on the Linux host
|
||||||
|
- Problem: very complex, requires managing WIM indexes, boot.wim, etc.
|
||||||
|
- Problem: needs a "worker" Windows VM just to run DISM
|
||||||
|
|
||||||
|
3. **Offline .msu approach**: Considered downloading specific .msu files from Microsoft Update Catalog:
|
||||||
|
- Search catalog.update.microsoft.com for `cumulative update for windows 10 version 21H2 x64`
|
||||||
|
- Found latest: KB5087544 (May 2026), KB5088859 (.NET), KB2267602 (Defender)
|
||||||
|
- Problem: catalog uses JavaScript popups for downloads — can't scrape URLs
|
||||||
|
- Could use `curl` POST to `DownloadDialog.aspx` with update GUIDs, but GUIDs are hard to extract
|
||||||
|
- The download URLs from `catalog.s.download.windowsupdate.com` are stable (don't expire) but finding them requires browser interaction
|
||||||
|
|
||||||
|
### Phase 2: customizeImage Template (adopted)
|
||||||
|
User said: "instead of iso, let's create a customizeImage template that basically updates the existing image and runs qcow2 compact on it"
|
||||||
|
|
||||||
|
This is much simpler — apply updates to the running Windows image during Audit Mode, not to the ISO.
|
||||||
|
|
||||||
|
### Phase 3: Online vs Offline Updates
|
||||||
|
Initially built the template with two modes:
|
||||||
|
- **Offline mode**: user provides `.msu`/`.cab` files as `fetchurl` derivations, applied via `DISM /Online /Add-Package`
|
||||||
|
- **Online mode**: triggers Windows Update service directly via COM API
|
||||||
|
|
||||||
|
User said "just do online updates" — removed offline mode entirely.
|
||||||
|
|
||||||
|
## What was built
|
||||||
|
|
||||||
|
### New files
|
||||||
|
- `lib/images/windows/templates/essentials/windows-update.nix` — template that boots into Audit Mode, runs Windows Update via COM API, handles reboots automatically, compacts image
|
||||||
|
|
||||||
|
### Modified files
|
||||||
|
- `lib/images/windows/helpers/customizeImage.nix` — added `compact`, `qemuTimeout` parameters (committed in `3b454b7` on master alongside other changes)
|
||||||
|
- `lib/images/windows/templates/default.nix` — wired `windowsUpdate` into `essentials`
|
||||||
|
- `lib/images/windows/win10/images.nix` — added `updated` step between `upstream` and `basic`
|
||||||
|
- `lib/images/windows/win11/images.nix` — same
|
||||||
|
|
||||||
|
### New customizeImage parameters
|
||||||
|
|
||||||
|
#### `compact ? false`
|
||||||
|
Added to `customizeImage.nix`. When true, runs `qemu-img convert -O qcow2` after the audit boot to flatten the COW chain into a standalone qcow2 with no backing file dependency. This is important for the update template because the updates add several GB of data to the COW overlay.
|
||||||
|
|
||||||
|
Implementation in `builderCommand`:
|
||||||
|
```bash
|
||||||
|
${lib.optionalString compact ''
|
||||||
|
echo "=== vmix: compacting image ==="
|
||||||
|
qemu-img convert -O qcow2 ${resultImg} compact.qcow2
|
||||||
|
mv compact.qcow2 ${resultImg}
|
||||||
|
''}
|
||||||
|
mv ${resultImg} $out
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `qemuTimeout ? 1800`
|
||||||
|
Added to `customizeImage.nix`. Replaces the hardcoded `timeout 1800` in the QEMU boot command. The Windows Update template sets this to `7200` (2 hours) because update installation with reboots can take a long time.
|
||||||
|
|
||||||
|
Both `timeout` invocations in the builder (primary and SDL-fallback) now use `${toString qemuTimeout}`.
|
||||||
|
|
||||||
|
### How the template works
|
||||||
|
|
||||||
|
The template is a function that takes `{ maxRounds ? 3 }` and returns a customizeImage-compatible attrset:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
name = "windows-update";
|
||||||
|
compact = true;
|
||||||
|
memSize = 4096;
|
||||||
|
qemuTimeout = 7200;
|
||||||
|
auditScript = "...";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Audit script flow
|
||||||
|
|
||||||
|
1. **Round tracking**: Uses `C:\vmix-update-round.txt` to track which round we're on across reboots. First run creates the file with "1", subsequent runs read and increment.
|
||||||
|
|
||||||
|
2. **Service initialization**:
|
||||||
|
```batch
|
||||||
|
net start wuauserv 2>nul
|
||||||
|
sc config wuauserv start= auto
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||||
|
```
|
||||||
|
The LTSC image may have update-blocking group policies. We delete them. We also wait 30 seconds on first round for the service to initialize and sync with Microsoft.
|
||||||
|
|
||||||
|
3. **PowerShell COM API**: The core update logic uses `Microsoft.Update.Session`:
|
||||||
|
```powershell
|
||||||
|
$session = New-Object -ComObject Microsoft.Update.Session
|
||||||
|
$searcher = $session.CreateUpdateSearcher()
|
||||||
|
$result = $searcher.Search('IsInstalled=0')
|
||||||
|
# ... accept EULAs, download, install ...
|
||||||
|
if ($installResult.RebootRequired) { exit 3010 } else { exit 0 }
|
||||||
|
```
|
||||||
|
- Exit code `3010` = reboot required
|
||||||
|
- Exit code `0` = no reboot needed (or no updates found)
|
||||||
|
- Exit code `1` = error (caught by try/catch)
|
||||||
|
- Per-update results are logged with HResult codes
|
||||||
|
|
||||||
|
4. **Component cleanup**: After updates, runs `dism /Online /Cleanup-Image /StartComponentCleanup /ResetBase /Quiet` to reclaim space from superseded update components.
|
||||||
|
|
||||||
|
5. **Reboot handling** (if exit code 3010 and round < maxRounds):
|
||||||
|
```batch
|
||||||
|
:: Copy script to survive wrapper deletion
|
||||||
|
copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
|
||||||
|
:: Register for next boot
|
||||||
|
reg add "HKLM\...\RunOnce" /v vmixUpdate /d "cmd /c C:\vmix-update-continue.cmd" /f
|
||||||
|
:: Preserve Audit Mode
|
||||||
|
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||||
|
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||||
|
:: Reboot
|
||||||
|
shutdown /r /f /t 0
|
||||||
|
exit /b
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Final shutdown**: When all rounds complete (or no more updates), the script shuts down:
|
||||||
|
```batch
|
||||||
|
del /q "%ROUND_FILE%" 2>nul
|
||||||
|
del /q "C:\vmix-update-continue.cmd" 2>nul
|
||||||
|
echo === vmix: Windows Update complete ===
|
||||||
|
shutdown /s /f /t 10 /c "vmix: windows-update complete"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipeline integration
|
||||||
|
|
||||||
|
#### win10/images.nix
|
||||||
|
```nix
|
||||||
|
ltsc = rec {
|
||||||
|
upstream = makeImage { ... };
|
||||||
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated (with templates; [ ... ]);
|
||||||
|
# ... withApps, withAMDGPU ...
|
||||||
|
};
|
||||||
|
laptopUpstream = makeImage { ... useAHCI = true; };
|
||||||
|
laptopUpdated = customizeImage laptopUpstream (templates.essentials.windowsUpdate {});
|
||||||
|
laptopSlim = customizeImageFold laptopUpdated templates.bundles.laptopSlim;
|
||||||
|
laptop = customizeImageFold laptopUpdated templates.bundles.laptop;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### win11/images.nix
|
||||||
|
Same pattern — `updated` step inserted between `upstream` and `basic`, `laptopUpdated` between `laptopUpstream` and `laptopSlim`/`laptop`.
|
||||||
|
|
||||||
|
### Usage examples
|
||||||
|
```nix
|
||||||
|
# Online mode (default) - triggers Windows Update service
|
||||||
|
essentials.windowsUpdate {}
|
||||||
|
essentials.windowsUpdate { maxRounds = 5; }
|
||||||
|
|
||||||
|
# In image pipeline
|
||||||
|
updated = customizeImage upstream (templates.essentials.windowsUpdate {});
|
||||||
|
basic = customizeImageFold updated [ ... ];
|
||||||
|
|
||||||
|
# Override VNC display and disable compact for debugging
|
||||||
|
vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; compact = false; })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detailed Test Log
|
||||||
|
|
||||||
|
### Build 1: First attempt (no VNC, wrong attr path)
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
in vmixLib.images.win10.ltsc.updated
|
||||||
|
'
|
||||||
|
```
|
||||||
|
**Result**: Error — `attribute 'images' missing`. The correct path is `vmixLib.windows.images.win10.ltsc.updated`, not `vmixLib.images.win10.ltsc.updated`.
|
||||||
|
|
||||||
|
### Build 2: Correct path, VNC :1, original script (no wuauserv start)
|
||||||
|
```bash
|
||||||
|
nix build ... --expr '
|
||||||
|
let vmixLib = ...;
|
||||||
|
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||||
|
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||||
|
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":1"; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
**Result**: Build "succeeded" but image size identical to upstream (5.03 GB vs 5.02 GB). The COM API found 0 updates because:
|
||||||
|
- Windows Update service (`wuauserv`) wasn't running in Audit Mode
|
||||||
|
- No time given for service to initialize
|
||||||
|
- Possible update-blocking policies active
|
||||||
|
|
||||||
|
**Build log showed**: QEMU booted, script ran, shut down — no errors visible in nix log (VNC output isn't captured).
|
||||||
|
|
||||||
|
### Build 3: Added wuauserv start, policy removal, 30s wait
|
||||||
|
Updated `windows-update.nix` to add:
|
||||||
|
```batch
|
||||||
|
net start wuauserv 2>nul
|
||||||
|
sc config wuauserv start= auto
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||||
|
timeout /t 30 /nobreak >nul
|
||||||
|
```
|
||||||
|
|
||||||
|
Also added EULA acceptance and try/catch error handling to the PowerShell block.
|
||||||
|
|
||||||
|
**Build with VNC :55, chained virtio tools first**:
|
||||||
|
```bash
|
||||||
|
nix build ... --expr '
|
||||||
|
let vmixLib = ...;
|
||||||
|
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||||
|
withVirtio = vmixLib.windows.customizeImage upstream vmixLib.windows.templates.essentials.virtioTools;
|
||||||
|
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||||
|
in vmixLib.windows.customizeImage withVirtio (windowsUpdate // { vncDisplay = ":55"; compact = false; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
**VNC observations** (screenshots taken with `gvnccapture localhost:55 /tmp/screenshot.png`):
|
||||||
|
|
||||||
|
- **T+2min**: Boot screen "Please wait"
|
||||||
|
- **T+3min**: CMD window showing:
|
||||||
|
```
|
||||||
|
=== vmix audit: windows-update ===
|
||||||
|
=== vmix: Windows Update round 1 of 3 ===
|
||||||
|
The Windows Update service is starting.
|
||||||
|
The Windows Update service was started successfully.
|
||||||
|
[SC] ChangeServiceConfig SUCCESS
|
||||||
|
Waiting for Windows Update service to initialize...
|
||||||
|
Searching for updates...
|
||||||
|
```
|
||||||
|
- **T+7min**: Found 6 updates:
|
||||||
|
```
|
||||||
|
Found 6 updates
|
||||||
|
- 2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8 for Windows 10 Version 21H2 for x64 (KB5010472)
|
||||||
|
- Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64 (KB5011048)
|
||||||
|
- Windows Malicious Software Removal Tool x64 - v5.141 (KB890830)
|
||||||
|
- 2026-05 Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1 for Windows 10 Version 21H2 for x64 (KB5088859)
|
||||||
|
- Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.451.326.0) - Current Channel (Broad)
|
||||||
|
- 2026-05 Cumulative Update for Windows 10 Version 21H2 for x64-based Systems (KB5087544)
|
||||||
|
Downloading...
|
||||||
|
```
|
||||||
|
- **T+12min**: "Installing..."
|
||||||
|
- **T+20min through T+55min**: Still "Installing..." — KB5087544 is a massive cumulative update spanning Oct 2021 → May 2026
|
||||||
|
- **T+58min**: Sysprep dialog showing on desktop — the machine rebooted after update install and landed in Audit Mode desktop, but the round 2 script didn't run
|
||||||
|
|
||||||
|
**Issue discovered**: The wrapper script (`vmix-audit-wrapper.cmd`) deletes `C:\vmix-audit-script.cmd` after the audit script returns but before the reboot completes. The RunOnce entry points to the deleted file.
|
||||||
|
|
||||||
|
### Build 4: Added script copy fix + shutdown fix
|
||||||
|
|
||||||
|
Updated script to:
|
||||||
|
1. Copy itself to `C:\vmix-update-continue.cmd` before rebooting
|
||||||
|
2. Register `cmd /c C:\vmix-update-continue.cmd` in RunOnce (not the original path)
|
||||||
|
3. Always call `shutdown /s /f /t 10` when done (handles both wrapper and RunOnce invocation)
|
||||||
|
|
||||||
|
**Same build command as Build 3.**
|
||||||
|
|
||||||
|
**VNC observations**:
|
||||||
|
- **T+3min**: Round 1 started, same 6 updates found
|
||||||
|
- **T+7min**: Downloading...
|
||||||
|
- **T+12min**: Installing...
|
||||||
|
- **T+55min**: TianoCore UEFI boot screen — machine rebooted!
|
||||||
|
- **T+57min**: "Working on updates — 90% complete" — Windows finalizing update installation after reboot
|
||||||
|
- **T+62min**: "Working on updates — 19% complete" (different counter — this is the post-reboot finalization)
|
||||||
|
- **T+70min**: Sysprep dialog... but NO round 2 script running
|
||||||
|
|
||||||
|
**Issue discovered**: After reboot, Windows exited Audit Mode and entered OOBE ("Choose your keyboard layout" screen) instead of staying in Audit Mode. The cumulative update reset the Audit Mode flags during its finalization pass.
|
||||||
|
|
||||||
|
Wait — actually on this build we saw the Sysprep dialog (which IS Audit Mode). The issue was the RunOnce not firing. On a subsequent build, we saw the keyboard layout screen (OOBE). The behavior is inconsistent.
|
||||||
|
|
||||||
|
### Build 5: Added Audit Mode preservation
|
||||||
|
|
||||||
|
Added registry keys before reboot to preserve Audit Mode:
|
||||||
|
```batch
|
||||||
|
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||||
|
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Same build command. VNC observations**:
|
||||||
|
- **T+3min**: Round 1 started, same 6 updates found
|
||||||
|
- **T+70min**: Still on "Installing..." — round 1 didn't finish in 70 min on this machine
|
||||||
|
- **T+100min**: "Working on updates — 19% complete" — rebooted, finalizing
|
||||||
|
- **T+110min**: "Choose your keyboard layout" — OOBE screen again!
|
||||||
|
|
||||||
|
**Session ended here** — the Audit Mode preservation fix hasn't been verified as working yet. The machine was moved to a faster machine for continued testing.
|
||||||
|
|
||||||
|
## Detailed Issue Analysis
|
||||||
|
|
||||||
|
### Issue 1: Windows Update service not running
|
||||||
|
**Root cause**: In Audit Mode, the Windows Update service (`wuauserv`) has `Start=3` (manual) and isn't auto-started. The COM API requires the service to be running to search for updates.
|
||||||
|
|
||||||
|
**Fix**: Explicitly start the service and set it to auto-start:
|
||||||
|
```batch
|
||||||
|
net start wuauserv 2>nul
|
||||||
|
sc config wuauserv start= auto
|
||||||
|
```
|
||||||
|
|
||||||
|
Also remove any group policies that block updates (LTSC may have these pre-configured):
|
||||||
|
```batch
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" /f 2>nul
|
||||||
|
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /f 2>nul
|
||||||
|
```
|
||||||
|
|
||||||
|
And wait 30 seconds for the service to initialize and sync with Microsoft servers on first round.
|
||||||
|
|
||||||
|
### Issue 2: Wrapper deletes script before reboot completes
|
||||||
|
**Root cause**: The `customizeImage.nix` wrapper script flow:
|
||||||
|
```
|
||||||
|
call C:\vmix-audit-script.cmd ← our update script
|
||||||
|
del /q C:\vmix-audit-script.cmd ← DELETE happens here
|
||||||
|
shutdown /s /t 5 ← may override our shutdown /r
|
||||||
|
del /q C:\vmix-audit-wrapper.cmd
|
||||||
|
```
|
||||||
|
|
||||||
|
When our script calls `shutdown /r /f /t 0` and then `exit /b`, control returns to the wrapper. The wrapper deletes the script file. Even though `shutdown /r /t 0` was called, the batch file continues executing and the delete happens before the system actually reboots.
|
||||||
|
|
||||||
|
The RunOnce entry points to `C:\vmix-audit-script.cmd` which no longer exists after reboot.
|
||||||
|
|
||||||
|
Additionally, the wrapper's `shutdown /s /t 5` may override our `shutdown /r /t 0` (though in practice the `/t 0` reboot usually wins).
|
||||||
|
|
||||||
|
**Fix**: Copy the script to a separate path before rebooting:
|
||||||
|
```batch
|
||||||
|
copy /y "%~f0" "C:\vmix-update-continue.cmd" >nul
|
||||||
|
reg add "HKLM\...\RunOnce" /v vmixUpdate /d "cmd /c C:\vmix-update-continue.cmd" /f
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrapper only knows about `C:\vmix-audit-script.cmd` and `C:\vmix-audit-wrapper.cmd`. It doesn't know about `C:\vmix-update-continue.cmd`, so that file survives.
|
||||||
|
|
||||||
|
### Issue 3: Audit Mode lost after update reboot
|
||||||
|
**Root cause**: Windows Audit Mode is maintained by registry keys:
|
||||||
|
- `HKLM\SYSTEM\Setup\Status\AuditBoot` (AuditBoot = 1)
|
||||||
|
- `HKLM\SYSTEM\Setup` (AuditInProgress = 1)
|
||||||
|
|
||||||
|
Some cumulative updates include "specialize" or "generalize" passes during their finalization that can clear these keys, causing Windows to transition from Audit Mode to OOBE on the next boot.
|
||||||
|
|
||||||
|
**Fix**: Explicitly set the Audit Mode registry keys right before rebooting:
|
||||||
|
```batch
|
||||||
|
reg add "HKLM\SYSTEM\Setup\Status\AuditBoot" /v AuditBoot /t REG_DWORD /d 1 /f
|
||||||
|
reg add "HKLM\SYSTEM\Setup" /v AuditInProgress /t REG_DWORD /d 1 /f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status**: NOT YET VERIFIED. The last build with this fix was still in the install phase when the session ended. The fix may or may not be sufficient — some updates may clear these keys AFTER the reboot during the "Working on updates X% complete" phase, which would happen after our registry writes.
|
||||||
|
|
||||||
|
**Alternative approaches if the fix doesn't work**:
|
||||||
|
1. Use `C:\Windows\System32\Sysprep\sysprep.exe /audit /reboot /quiet` instead of `shutdown /r` — this explicitly tells Windows to re-enter Audit Mode
|
||||||
|
2. Set up a `SetupComplete.cmd` or `Specialize` unattend pass that forces Audit Mode
|
||||||
|
3. Use a scheduled task instead of RunOnce (scheduled tasks persist across mode transitions)
|
||||||
|
4. Accept single-round updates only (`maxRounds = 1`) — no reboot needed if updates don't require it
|
||||||
|
|
||||||
|
### Issue 4: No shutdown after round 2
|
||||||
|
**Root cause**: When the script is invoked via RunOnce (round 2+), it runs directly — not through the wrapper. The wrapper is what normally calls `shutdown /s /t 5` after the script completes. Without the wrapper, the script finishes and the machine just sits at the Audit Mode desktop indefinitely (until the 2-hour QEMU timeout).
|
||||||
|
|
||||||
|
**Fix**: The script itself calls `shutdown /s /f /t 10` when all rounds are complete. This is harmless when called from the wrapper (two shutdown commands — the second one either fails silently or is a no-op since shutdown is already pending).
|
||||||
|
|
||||||
|
## All Build Commands Used
|
||||||
|
|
||||||
|
### Quick evaluation (check attribute paths)
|
||||||
|
```bash
|
||||||
|
nix eval --impure --expr '
|
||||||
|
let vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
in builtins.attrNames vmixLib.windows.images.win10.ltsc
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build updated image directly (no VNC, with compact)
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let
|
||||||
|
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
in vmixLib.windows.images.win10.ltsc.updated
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build with VNC monitoring on port 5901 (display :1)
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let
|
||||||
|
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||||
|
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||||
|
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":1"; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build with virtio tools + VNC :55 + no compact (debugging)
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let
|
||||||
|
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||||
|
withVirtio = vmixLib.windows.customizeImage upstream vmixLib.windows.templates.essentials.virtioTools;
|
||||||
|
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||||
|
in vmixLib.windows.customizeImage withVirtio (windowsUpdate // { vncDisplay = ":55"; compact = false; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build with more RAM (8GB) for faster installs
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let
|
||||||
|
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||||
|
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate {};
|
||||||
|
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; memSize = 8192; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build with more rounds
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let
|
||||||
|
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
upstream = vmixLib.windows.images.win10.ltsc.upstream;
|
||||||
|
windowsUpdate = vmixLib.windows.templates.essentials.windowsUpdate { maxRounds = 5; };
|
||||||
|
in vmixLib.windows.customizeImage upstream (windowsUpdate // { vncDisplay = ":55"; })
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full pipeline test (upstream → updated → basic → generalize)
|
||||||
|
```bash
|
||||||
|
nix build --print-build-logs --impure --option sandbox relaxed --expr '
|
||||||
|
let
|
||||||
|
vmixLib = (builtins.getFlake "path:/storage/gitrepos/vmix.nix").lib.x86_64-linux;
|
||||||
|
in vmixLib.windows.images.win10.ltsc.basic.generalize {
|
||||||
|
username = "User"; password = ""; hostname = "WIN-VM";
|
||||||
|
vncDisplay = ":55";
|
||||||
|
}
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### VNC monitoring commands
|
||||||
|
```bash
|
||||||
|
# Take a screenshot
|
||||||
|
nix-shell -p gtk-vnc --run 'gvnccapture localhost:55 /tmp/screenshot.png'
|
||||||
|
|
||||||
|
# Connect with a VNC viewer
|
||||||
|
vncviewer localhost:5955
|
||||||
|
|
||||||
|
# Check if QEMU is running
|
||||||
|
ps aux | grep 'qemu-system' | grep -v grep
|
||||||
|
|
||||||
|
# Check nix build log for a store path
|
||||||
|
nix log /nix/store/HASH-windows-update-....qcow2
|
||||||
|
|
||||||
|
# Check image info
|
||||||
|
nix-shell -p qemu --run 'qemu-img info /nix/store/HASH-....qcow2'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image sizes observed
|
||||||
|
| Image | disk size | virtual size |
|
||||||
|
|-------|-----------|-------------|
|
||||||
|
| upstream (win10-ltsc-2021) | 5.02 GB | 64 GB |
|
||||||
|
| updated (killed build, round 1 only) | 8.33 GB | 64 GB |
|
||||||
|
| upstream (compacted, no updates) | 5.03 GB | 64 GB |
|
||||||
|
|
||||||
|
## Updates found by Windows Update (Win10 LTSC 2021 → May 2026)
|
||||||
|
1. **2022-02 Cumulative Update Preview for .NET Framework 3.5 and 4.8** (KB5010472)
|
||||||
|
2. **Microsoft .NET Framework 4.8.1** for Windows 10 21H2 x64 (KB5011048)
|
||||||
|
3. **Windows Malicious Software Removal Tool x64 v5.141** (KB890830)
|
||||||
|
4. **2026-05 Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1** (KB5088859)
|
||||||
|
5. **Security Intelligence Update for Microsoft Defender Antivirus** (KB2267602, Version 1.451.326.0+)
|
||||||
|
6. **2026-05 Cumulative Update for Windows 10 Version 21H2** (KB5087544) — the big one, ~870 MB
|
||||||
|
|
||||||
|
## Timing observations (4 vCPU, 4GB RAM machine)
|
||||||
|
- Nix evaluation: ~30 seconds
|
||||||
|
- VirtIO tools step (cached): instant
|
||||||
|
- Windows boot to Audit Mode desktop: ~60 seconds
|
||||||
|
- Windows Update search: ~2-3 minutes
|
||||||
|
- Download all 6 updates: ~3-5 minutes
|
||||||
|
- Install all updates (especially KB5087544): **50-70 minutes**
|
||||||
|
- Reboot + "Working on updates" finalization: ~10-15 minutes
|
||||||
|
- Total for round 1: ~70-90 minutes
|
||||||
|
- DISM cleanup: ~2-5 minutes
|
||||||
|
- qemu-img convert (compact): ~2-3 minutes
|
||||||
|
|
||||||
|
## Remaining TODO (for next session on faster machine)
|
||||||
|
- [ ] **CRITICAL**: Verify the Audit Mode preservation fix works (AuditBoot + AuditInProgress registry keys)
|
||||||
|
- [ ] If Audit Mode fix doesn't work, try `sysprep /audit /reboot /quiet` instead of `shutdown /r`
|
||||||
|
- [ ] Test round 2 → round 3 flow (find additional updates after cumulative? likely Defender definition updates)
|
||||||
|
- [ ] Test with `compact = true` to verify final standalone image
|
||||||
|
- [ ] Test full pipeline: `upstream → updated → basic → generalize`
|
||||||
|
- [ ] Test win11 images
|
||||||
|
- [ ] Consider using 8GB RAM (`memSize = 8192`) for faster update installs
|
||||||
|
- [ ] Consider: should `windowsUpdate` go before or after `virtioTools`? Currently before in the pipeline, but the explicit build command chains virtio first for better I/O
|
||||||
|
- [ ] The template is impure (downloads from Microsoft during build) — this is intentional but should be documented
|
||||||
|
- [ ] Consider `maxRounds = 1` mode for builds where you only want non-reboot updates
|
||||||
|
- [ ] The `compact` step doesn't compress — consider adding `-c` flag to `qemu-img convert` for compressed qcow2
|
||||||
|
- [ ] The wrapper's `shutdown /s /t 5` may still race with the script's `shutdown /r /t 0` — consider using `shutdown /a` (abort) before `shutdown /r` to cancel any pending shutdown
|
||||||
|
|
||||||
|
## Architecture notes
|
||||||
|
|
||||||
|
### How customizeImage works (for context)
|
||||||
|
1. Creates a COW overlay on the original image: `qemu-img create -f qcow2 -b ${originalImage} -F qcow2 disk.qcow2`
|
||||||
|
2. Optionally resizes: `qemu-img resize disk.qcow2 ${diskSize}`
|
||||||
|
3. Merges offline registry entries via `virt-win-reg --merge`
|
||||||
|
4. Injects audit script + wrapper via `virt-customize --upload`
|
||||||
|
5. Adds RunOnce registry entry for the wrapper
|
||||||
|
6. Boots QEMU with the image (user networking, UEFI, VirtIO or AHCI)
|
||||||
|
7. The wrapper runs the audit script, then shuts down
|
||||||
|
8. Optionally compacts: `qemu-img convert -O qcow2`
|
||||||
|
9. Moves result to `$out`
|
||||||
|
|
||||||
|
### The wrapper problem
|
||||||
|
The wrapper (`vmix-audit-wrapper.cmd`) is designed for simple, single-boot templates:
|
||||||
|
```batch
|
||||||
|
call C:\vmix-audit-script.cmd
|
||||||
|
del /q C:\vmix-audit-script.cmd
|
||||||
|
shutdown /s /t 5
|
||||||
|
del /q C:\vmix-audit-wrapper.cmd
|
||||||
|
```
|
||||||
|
|
||||||
|
This is fine for templates that don't reboot. But the Windows Update template needs multiple reboots, which conflicts with the wrapper's assumptions. The workarounds (copy script, self-shutdown) are necessary because modifying the wrapper would affect all templates.
|
||||||
|
|
||||||
|
A future improvement might be to add a `multiboot` flag to customizeImage that changes the wrapper behavior for templates that need reboots.
|
||||||
|
|
||||||
|
### Why QEMU user networking works for Windows Update
|
||||||
|
QEMU's `-nic user` (SLIRP) provides NAT networking. The guest gets DHCP and can reach the internet via the host. Windows Update uses HTTPS to Microsoft's servers, which works through NAT. No special firewall rules or port forwarding needed.
|
||||||
|
|
||||||
|
### Why VirtIO tools are chained before updates (in test builds)
|
||||||
|
The upstream image uses VirtIO storage (`if=virtio`) but doesn't have VirtIO guest tools installed. The update template doesn't need guest tools to work, but having them improves:
|
||||||
|
- Disk I/O performance (VirtIO balloon, better driver)
|
||||||
|
- Memory management
|
||||||
|
- Guest agent for monitoring
|
||||||
|
|
||||||
|
In the pipeline (`win10/images.nix`), updates are applied directly to `upstream` without virtio tools, because virtio tools installation is part of the `basic` step. For testing, we chain them explicitly.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue