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>
6.4 KiB
vm-images
Shared, locally cached virtual-machine installation media and base images. Downloaded ISO, QCOW2, IMG, VHD, and VHDX files are intentionally excluded from Git.
Download images
Download every supported image:
./download-images.sh
Download or check selected targets:
./download-images.sh debian13
./download-images.sh almalinux10
./download-images.sh fedora44
./download-images.sh ubuntu2604
./download-images.sh virtio-win
The Windows installation ISO is not downloaded because Microsoft's temporary
download URLs expire. Place it manually in install-iso/; the script finds it
automatically by matching Win, 11, the architecture (x64, or arm64 on
arm64 hosts) and .iso in that order (*Win*11*x64*.iso or
*Win*11*arm64*.iso, case-insensitive). If several files match, the highest
version-sorted name is used. Check which file was picked with
./download-images.sh --print-path windows11 --iso.
To get a fresh temporary URL, nodejs/get-windows-iso-url.js drives Microsoft's
download page with headless Chromium (needs Node.js and an installed
Chromium, for example from Playwright):
cd nodejs
npm ci # once, installs playwright-core
node get-windows-iso-url.js # print the URL for the host architecture
node get-windows-iso-url.js --arch arm64 # choose the architecture explicitly
The script only prints the URL. ./download-images.sh windows11 downloads that
URL into install-iso/ like the other images (aria2c, resumable) for the image
architecture (VM_IMAGE_ARCH=arm64 for arm64), and skips it when a matching ISO
is already there. The all target does not include it.
The URL expires after about 24 hours and may be bound to the IP that requested
it, so download from the machine that ran the script. Run it with --help for
the language and path options.
Query image paths
The path interface prints one absolute path without downloading anything:
./download-images.sh --print-path debian13
./download-images.sh --print-path almalinux10
./download-images.sh --print-path fedora44
./download-images.sh --print-path ubuntu2604
./download-images.sh --print-path virtio-win
./download-images.sh --print-path windows11
./download-images.sh --print-path windows11 --iso
The Windows target prints the QCOW2 path by default. Add --iso to print its
manually supplied installation ISO path. The QCOW2 is named after the detected
ISO (for example Win11_25H2_English_x64_v2.iso →
base-qcow2/Win11_25H2_English_x64_v2.qcow2); if no ISO is present, an
existing *Win*11*x64*.qcow2 (*Win*11*arm64*.qcow2 on arm64) in base-qcow2/ is used instead.
Download URLs can be overridden with DEBIAN_IMAGE_URL, ALMA_IMAGE_URL,
FEDORA_IMAGE_URL, UBUNTU_IMAGE_URL, VIRTIO_WIN_ISO_URL, and
OPENSSH_WIN_ZIP_URL. Each may
list several mirrors of the same file, separated by whitespace; the Fedora
image defaults to the TUNA, BFSU and NJU mirrors, and the Ubuntu image to the
TUNA, NJU and official cloud-images.ubuntu.com mirrors.
When aria2 is installed (aria2c), an image is
downloaded from all of its mirrors at once, with a single connection to each
mirror. Without it, curl (or wget) tries the mirrors in order. A partial
download is only resumed with the same downloader and mirror list; otherwise
it starts over. The Fedora and Ubuntu checksums can be overridden with
FEDORA_IMAGE_SHA256 and UBUNTU_IMAGE_SHA256 when using a custom image.
The openssh-win target downloads the Win32-OpenSSH zip matching the
architecture into tools-iso/: OpenSSH-Win64.zip on amd64 hosts and
OpenSSH-ARM64.zip on arm64 hosts.
Windows installation disk image
QEMU's emulated USB CD-ROM reads the Windows ISO at only a few MB/s on macOS.
build-windows-install-disk.sh builds a bootable FAT32 disk image from the ISO
(install.wim is split into install.swm parts because of the 4 GB FAT32 file
limit); attached as an NVMe disk it is read about 15 times faster.
# macOS (Homebrew), once
brew install wimlib dosfstools gptfdisk mtools
# Debian, once (p7zip-full or udisks2 only for non-root use)
sudo apt install wimtools dosfstools gdisk mtools p7zip-full
./build-windows-install-disk.sh [--force] install-iso/Windows11_..._arm64_....iso
The output defaults to the ISO path with .img in place of .iso. The
image is built without root. The ISO has to be read as UDF (its ISO 9660 side only
holds a README): macOS uses hdiutil, Linux uses mount -t udf as root,
otherwise udisksctl, otherwise 7z extracts it into a temporary directory
(which needs the free space of the whole ISO). The *.img file is ignored by Git.
Windows base image
A VM disk can be created as a QCOW2 overlay on a Windows base image, so a new VM
does not run Windows Setup again. build-windows-base.sh makes that base image
(by default at the path ./download-images.sh --print-path windows11 prints): it
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), 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 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).
It needs qemu, qemu-img, an ISO tool (xorriso, genisoimage or mkisofs)
and socat or nc; there is no SSH to the guest.
./build-windows-base.sh [--force] [--compress] [--from-img] [--headless] [OUTPUT.qcow2]
It refuses to replace an existing base image without --force, because the
overlays of existing VMs still point at that file. Build to another OUTPUT
(for example with a version in its name) and point your VM launcher at it. The installation runs in a scratch directory
/var/tmp/build-windows-base.* (set BASE_BUILD_TMPDIR to use another parent; about 25 GB are needed).