From 00695cdddcdd6e1c11d34e67213ad6478ec92983 Mon Sep 17 00:00:00 2001 From: Guofang Date: Mon, 2 Nov 2026 22:09:05 +0800 Subject: [PATCH] Install the VirtIO drivers into the base image in audit mode Add Install-VirtIO-Drivers.ps1, run from Autounattend.xml before sysprep, and attach the virtio-win ISO (downloaded if missing) during the build. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 7 +++-- build-windows-base.sh | 17 ++++++++-- windows11/seed/Autounattend.xml | 5 +++ windows11/seed/Install-VirtIO-Drivers.ps1 | 38 +++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 windows11/seed/Install-VirtIO-Drivers.ps1 diff --git a/README.md b/README.md index 0d0a501..d31d2c6 100644 --- a/README.md +++ b/README.md @@ -118,11 +118,12 @@ does not run Windows Setup again. `build-windows-base.sh` makes that base image installs Windows unattended from the ISO in `install-iso/` (it does everything itself: answer file in `windows11/seed/`, QEMU hardware in `windows11/qemu/`; this directory has no dependency on any other repository). The answer file boots -into audit mode (no account and no credentials are needed) and runs +into audit mode (no account and no credentials are needed), adds the VirtIO drivers +from the virtio-win ISO (downloaded if missing) to the driver store and runs `sysprep /generalize` there as its last step; the script waits for the guest to power off and stores the disk read-only. The image is clean and generic, like a cloud -image: nothing but Windows, with no account, no SSH, no settings, no VirtIO or -other hardware-specific drivers and no machine identity. Each VM is configured +image: nothing but Windows and the VirtIO drivers, with no account, no SSH, no settings, +no other hardware-specific drivers and no machine identity. Each VM is configured on its first boot: Windows reads `Unattend.xml` from the root of an attached CD-ROM during its specialize and OOBE passes (a VM launcher can mount such a seed ISO when it creates a VM disk). diff --git a/build-windows-base.sh b/build-windows-base.sh index 54525fd..2023d0b 100755 --- a/build-windows-base.sh +++ b/build-windows-base.sh @@ -6,11 +6,12 @@ # made once, and this script needs nothing outside this directory: # 1. an answer ISO is built from windows11/seed/Autounattend.xml; # 2. QEMU installs Windows unattended from the ISO into a scratch directory. -# The answer file then boots into audit mode, where it runs sysprep +# The answer file then boots into audit mode, where it adds the VirtIO +# drivers from the virtio-win ISO to the driver store and runs sysprep # /generalize, which removes the machine identity and shuts the guest down; # 3. the shut-down disk is converted into the output file and made read-only. -# The image holds nothing but Windows itself, like a cloud image: no VirtIO or -# other hardware-specific drivers (the installation hardware, windows11/qemu, +# The image holds nothing but Windows and the VirtIO drivers, like a cloud image: +# no other hardware-specific drivers (the installation hardware, windows11/qemu, # only uses devices with inbox Windows drivers), no machine identity, no user # account (the answer file holds no credentials either: audit mode needs no # account; the built-in Administrator is disabled), no SSH, no settings. Every @@ -58,6 +59,8 @@ Environment variables: VM_IMAGE_ARCH amd64 or arm64 (default: the host's) WIN11_VM_INSTALL_ISO Windows 11 ISO (default: the path that "download-images.sh --print-path windows11 --iso" prints) + WIN11_VM_TOOLS_ISO virtio-win ISO (default: the path that + "download-images.sh --print-path virtio-win" prints; downloaded if missing) WIN11_VM_DISK_SIZE Size of the system disk (default: 64G) WIN11_VM_UEFI_CODE OVMF read-only firmware (auto-detected if unset) WIN11_VM_UEFI_VARS OVMF variable-store template (auto-detected if unset) @@ -130,6 +133,11 @@ if [[ -z "${WIN11_VM_INSTALL_ISO:-}" ]] && ! "$downloader" --print-path windows1 fi fi +# The VirtIO drivers for the image come from the virtio-win ISO. +virtio_iso="${WIN11_VM_TOOLS_ISO:-$("$downloader" --print-path virtio-win)}" +[[ -s "$virtio_iso" ]] || "$downloader" virtio-win || exit 1 +[[ -s "$virtio_iso" ]] || die "the virtio-win ISO was not found: ${virtio_iso}" + output="${output:-$("$downloader" --print-path windows11)}" if [[ -e "$output" && $force -eq 0 ]]; then die "${output} exists (it may be the backing file of existing VM disks); use --force or another OUTPUT." @@ -221,6 +229,7 @@ seed_dir="${work}/seed" mkdir -p "$seed_dir" sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${arch}\"/" \ "${win_dir}/seed/Autounattend.xml" >"${seed_dir}/Autounattend.xml" +cp -- "${win_dir}/seed/Install-VirtIO-Drivers.ps1" "${seed_dir}/" seed_iso="${work}/seed.iso" case "$iso_tool" in xorriso) xorriso -as mkisofs -quiet -o "$seed_iso" -V ANSWER -J -r "$seed_dir" ;; @@ -298,6 +307,8 @@ else attach_media install_media "$install_iso" 2 ide.1 fi attach_media answer_media "$seed_iso" 3 ide.2 +# The VirtIO drivers are installed into the image from this ISO in audit mode. +attach_media virtio_media "$virtio_iso" 4 ide.3 # The Windows ISO boots through bootmgr, which waits for a key ("Press any key # to boot from CD or DVD...") and gives up after a few seconds. Nobody is there diff --git a/windows11/seed/Autounattend.xml b/windows11/seed/Autounattend.xml index a9aeb61..7343e16 100644 --- a/windows11/seed/Autounattend.xml +++ b/windows11/seed/Autounattend.xml @@ -131,6 +131,11 @@ 4 + Add the VirtIO drivers from the virtio-win ISO to the driver store + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& ('{0}:\Install-VirtIO-Drivers.ps1' -f (Get-Volume -FileSystemLabel ANSWER).DriveLetter); exit $LASTEXITCODE" + + + 5 Generalize the image and power off C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /shutdown /quiet diff --git a/windows11/seed/Install-VirtIO-Drivers.ps1 b/windows11/seed/Install-VirtIO-Drivers.ps1 new file mode 100644 index 0000000..8db4a73 --- /dev/null +++ b/windows11/seed/Install-VirtIO-Drivers.ps1 @@ -0,0 +1,38 @@ +# Runs in audit mode, before sysprep: adds the VirtIO drivers from the virtio-win +# ISO to the driver store. The devices are not present during the installation +# (it uses ramfb and e1000e), so pnputil only stages the packages; they survive +# /generalize, and Windows binds them on the first start with virtio devices. +$ErrorActionPreference = 'Stop' + +$arch = if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'ARM64' } else { 'amd64' } +$roots = (Get-PSDrive -PSProvider FileSystem).Root +$drivers = @( + @{ Folder = 'NetKVM'; Inf = 'netkvm.inf'; Required = $true }, + @{ Folder = 'viogpudo'; Inf = 'viogpudo.inf'; Required = $true }, + @{ Folder = 'vioserial'; Inf = 'vioser.inf' }, + @{ Folder = 'Balloon'; Inf = 'balloon.inf' }, + @{ Folder = 'viorng'; Inf = 'viorng.inf' }, + @{ Folder = 'vioinput'; Inf = 'vioinput.inf' } +) + +$installed = 0 +foreach ($driver in $drivers) { + $inf = $roots | + ForEach-Object { Join-Path $_ "$($driver.Folder)\w11\$arch\$($driver.Inf)" } | + Where-Object { Test-Path -LiteralPath $_ } | + Select-Object -First 1 + if (-not $inf) { + if ($driver.Required) { + throw "The $arch $($driver.Folder) driver was not found on the attached media." + } + continue + } + + & pnputil.exe /add-driver $inf /install | Out-Host + # 3010 asks for a restart; 259 means that no device uses the driver. + if ($LASTEXITCODE -notin 0, 259, 3010) { + throw "pnputil failed to install $inf (exit code $LASTEXITCODE)." + } + $installed++ +} +Write-Host "Added $installed VirtIO $arch driver packages."