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 <noreply@anthropic.com>
This commit is contained in:
guofangandClaude Sonnet 5.5 committed 2026-11-02 22:09:05 +08:00
1 parent 91ae8cb147
commit 00695cdddc
4 files changed
+61 -6

No files matched your search

+4 -3
View File
@@ -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).
+14 -3
View File
@@ -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
+5
View File
@@ -131,6 +131,11 @@
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>4</Order>
<Description>Add the VirtIO drivers from the virtio-win ISO to the driver store</Description>
<Path>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "&amp; ('{0}:\Install-VirtIO-Drivers.ps1' -f (Get-Volume -FileSystemLabel ANSWER).DriveLetter); exit $LASTEXITCODE"</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>5</Order>
<Description>Generalize the image and power off</Description>
<Path>C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /shutdown /quiet</Path>
</RunSynchronousCommand>
+38
View File
@@ -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."