Files
vm-images/README.md
T
GuofangandClaude Sonnet 5.5 71c0c3d029 Add build-windows-base.sh to build the Windows 11 base image
Installs Windows unattended from the ISO in a scratch VM (under /var/tmp
by default), generalizes it with sysprep over SSH and stores the shut-down
disk as the read-only base image that run-windows11.py clones.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 16:12:23 +08:00

131 lines
5.6 KiB
Markdown

# 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:
```bash
./download-images.sh
```
Download or check selected targets:
```bash
./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):
```bash
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:
```bash
./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](https://aria2.github.io/) 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.
```bash
# 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
`run-windows11.py` creates a missing system disk as a QCOW2 overlay on the base
image that `./download-images.sh --print-path windows11` prints, so a new VM does
not run Windows Setup again. `build-windows-base.sh` makes that base image: it
installs Windows unattended from the ISO in `install-iso/`, generalizes it with
`sysprep` over SSH, and stores the shut-down disk read-only.
```bash
./build-windows-base.sh [--force] [--no-sysprep] [--compress] [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 select it with
`WIN11_VM_BASE_IMAGE` instead. 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) and needs a private key that matches
the public key put on the answer media.