Compare commits

...
8 Commits
Author SHA1 Message Date
GuofangandClaude Sonnet 5.5 384a4ade9f Add the rocky9 download target
Rocky Linux 9.8 GenericCloud Base for amd64 and arm64, with pinned SHA256
checksums, from dl.rockylinux.org and the NJU mirror.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 11:02:12 +08:00
GuofangandClaude Sonnet 5.5 6064bd650d Default the install prompts to yes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 23:36:15 +08:00
GuofangandClaude Sonnet 5.5 3935e2d5e2 Download Win32-OpenSSH through the wangguofang.net GitHub proxy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 09:28:34 +08:00
guofangandClaude Sonnet 5.5 00695cdddc 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>
2026-11-02 22:09:05 +08:00
guofangandClaude Sonnet 5.5 91ae8cb147 build-windows-base.sh: show a window by default, fix macOS bash 3.2 and fork crashes
Replace --window with --headless (VNC); the window is the default. Fill the
QEMU config placeholders without nested quotes (bash 3.2 kept them literally),
and run QEMU as a background job instead of -daemonize when it shows a window
(Cocoa crashes in fork()).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:31:10 +08:00
guofangandClaude Sonnet 5.5 4eb26e3a96 Download with 16 aria2c connections by default
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 21:00:02 +08:00
guofangandClaude Sonnet 5.5 1476b4a3de Prompt step by step for the missing base image, ISO, npm deps and Chromium
Offer to build the base QCOW2, download the ISO, run npm ci and install
Chromium (from the npmmirror mirror by default). Let Playwright locate its
own Chromium so macOS works.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 20:53:50 +08:00
guofangandClaude Sonnet 5.5 b37d1e002c Report a missing base QCOW2 image instead of a missing ISO
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 20:26:07 +08:00
6 changed files with 208 additions and 48 deletions

No files matched your search

+12 -8
View File
@@ -17,6 +17,7 @@ Download or check selected targets:
```bash ```bash
./download-images.sh debian13 ./download-images.sh debian13
./download-images.sh almalinux10 ./download-images.sh almalinux10
./download-images.sh rocky9
./download-images.sh fedora44 ./download-images.sh fedora44
./download-images.sh ubuntu2604 ./download-images.sh ubuntu2604
./download-images.sh virtio-win ./download-images.sh virtio-win
@@ -57,6 +58,7 @@ The path interface prints one absolute path without downloading anything:
```bash ```bash
./download-images.sh --print-path debian13 ./download-images.sh --print-path debian13
./download-images.sh --print-path almalinux10 ./download-images.sh --print-path almalinux10
./download-images.sh --print-path rocky9
./download-images.sh --print-path fedora44 ./download-images.sh --print-path fedora44
./download-images.sh --print-path ubuntu2604 ./download-images.sh --print-path ubuntu2604
./download-images.sh --print-path virtio-win ./download-images.sh --print-path virtio-win
@@ -71,18 +73,19 @@ ISO (for example `Win11_25H2_English_x64_v2.iso` →
existing `*Win*11*x64*.qcow2` (`*Win*11*arm64*.qcow2` on arm64) in `base-qcow2/` is used instead. 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`, Download URLs can be overridden with `DEBIAN_IMAGE_URL`, `ALMA_IMAGE_URL`,
`FEDORA_IMAGE_URL`, `UBUNTU_IMAGE_URL`, `VIRTIO_WIN_ISO_URL`, and `ROCKY_IMAGE_URL`, `FEDORA_IMAGE_URL`, `UBUNTU_IMAGE_URL`, `VIRTIO_WIN_ISO_URL`, and
`OPENSSH_WIN_ZIP_URL`. Each may `OPENSSH_WIN_ZIP_URL`. Each may
list several mirrors of the same file, separated by whitespace; the Fedora 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 image defaults to the TUNA, BFSU and NJU mirrors, the Rocky Linux image to the official
dl.rockylinux.org and NJU mirrors, and the Ubuntu image to the
TUNA, NJU and official cloud-images.ubuntu.com mirrors. TUNA, NJU and official cloud-images.ubuntu.com mirrors.
When [aria2](https://aria2.github.io/) is installed (`aria2c`), an image is 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 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 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 download is only resumed with the same downloader and mirror list; otherwise
it starts over. The Fedora and Ubuntu checksums can be overridden with it starts over. The Rocky Linux, Fedora and Ubuntu checksums can be overridden with
`FEDORA_IMAGE_SHA256` and `UBUNTU_IMAGE_SHA256` when using a custom image. `ROCKY_IMAGE_SHA256`, `FEDORA_IMAGE_SHA256` and `UBUNTU_IMAGE_SHA256` when using a custom image.
The `openssh-win` target downloads the Win32-OpenSSH zip matching the The `openssh-win` target downloads the Win32-OpenSSH zip matching the
architecture into `tools-iso/`: `OpenSSH-Win64.zip` on amd64 hosts and architecture into `tools-iso/`: `OpenSSH-Win64.zip` on amd64 hosts and
@@ -118,11 +121,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 installs Windows unattended from the ISO in `install-iso/` (it does everything
itself: answer file in `windows11/seed/`, QEMU hardware in `windows11/qemu/`; this itself: answer file in `windows11/seed/`, QEMU hardware in `windows11/qemu/`; this
directory has no dependency on any other repository). The answer file boots 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 `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 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 image: nothing but Windows and the VirtIO drivers, with no account, no SSH, no settings,
other hardware-specific drivers and no machine identity. Each VM is configured 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 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 CD-ROM during its specialize and OOBE passes (a VM launcher can mount such a
seed ISO when it creates a VM disk). seed ISO when it creates a VM disk).
@@ -130,7 +134,7 @@ It needs `qemu`, `qemu-img`, an ISO tool (`xorriso`, `genisoimage` or `mkisofs`)
and `socat` or `nc`; there is no SSH to the guest. and `socat` or `nc`; there is no SSH to the guest.
```bash ```bash
./build-windows-base.sh [--force] [--compress] [--from-img] [--window] [OUTPUT.qcow2] ./build-windows-base.sh [--force] [--compress] [--from-img] [--headless] [OUTPUT.qcow2]
``` ```
It refuses to replace an existing base image without `--force`, because the It refuses to replace an existing base image without `--force`, because the
+54 -15
View File
@@ -6,11 +6,12 @@
# made once, and this script needs nothing outside this directory: # made once, and this script needs nothing outside this directory:
# 1. an answer ISO is built from windows11/seed/Autounattend.xml; # 1. an answer ISO is built from windows11/seed/Autounattend.xml;
# 2. QEMU installs Windows unattended from the ISO into a scratch directory. # 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; # /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. # 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 # The image holds nothing but Windows and the VirtIO drivers, like a cloud image:
# other hardware-specific drivers (the installation hardware, windows11/qemu, # no other hardware-specific drivers (the installation hardware, windows11/qemu,
# only uses devices with inbox Windows drivers), no machine identity, no user # 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 answer file holds no credentials either: audit mode needs no
# account; the built-in Administrator is disabled), no SSH, no settings. Every # account; the built-in Administrator is disabled), no SSH, no settings. Every
@@ -24,19 +25,21 @@
# install-iso/ (see README.md). # install-iso/ (see README.md).
# #
# Usage: build-windows-base.sh [--force] [--keep-work] [--compress] [--from-img] # Usage: build-windows-base.sh [--force] [--keep-work] [--compress] [--from-img]
# [--window] [OUTPUT.qcow2] # [--headless] [OUTPUT.qcow2]
# OUTPUT defaults to the path that "download-images.sh --print-path windows11" # OUTPUT defaults to the path that "download-images.sh --print-path windows11"
# prints. # prints.
set -euo pipefail set -euo pipefail
images_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" images_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
downloader="${images_dir}/download-images.sh" downloader="${images_dir}/download-images.sh"
# Path lookups below must not offer to run this script again.
export VM_IMAGE_NO_PROMPT=1
win_dir="${images_dir}/windows11" win_dir="${images_dir}/windows11"
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") [--force] [--keep-work] [--compress] [--from-img] Usage: $(basename "$0") [--force] [--keep-work] [--compress] [--from-img]
[--window] [OUTPUT.qcow2] [--headless] [OUTPUT.qcow2]
Install Windows 11 from the ISO, generalize it with sysprep and store the disk Install Windows 11 from the ISO, generalize it with sysprep and store the disk
as the read-only, generic base image that VM disks are cloned from. as the read-only, generic base image that VM disks are cloned from.
@@ -47,18 +50,21 @@ as the read-only, generic base image that VM disks are cloned from.
--from-img Install from the disk image made by build-windows-install-disk.sh --from-img Install from the disk image made by build-windows-install-disk.sh
(the ISO path ending in .img), which reads much faster than (the ISO path ending in .img), which reads much faster than
the emulated CD-ROM the emulated CD-ROM
--window Show the guest in a QEMU window (needs a desktop session) --headless Serve the guest screen over VNC on 127.0.0.1:5990 instead of
instead of the VNC display on 127.0.0.1:5990 showing a QEMU window (the default; without a desktop
session on Linux, VNC is used as well)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
VM_IMAGE_ARCH amd64 or arm64 (default: the host's) VM_IMAGE_ARCH amd64 or arm64 (default: the host's)
WIN11_VM_INSTALL_ISO Windows 11 ISO (default: the path that WIN11_VM_INSTALL_ISO Windows 11 ISO (default: the path that
"download-images.sh --print-path windows11 --iso" prints) "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_DISK_SIZE Size of the system disk (default: 64G)
WIN11_VM_UEFI_CODE OVMF read-only firmware (auto-detected if unset) WIN11_VM_UEFI_CODE OVMF read-only firmware (auto-detected if unset)
WIN11_VM_UEFI_VARS OVMF variable-store template (auto-detected if unset) WIN11_VM_UEFI_VARS OVMF variable-store template (auto-detected if unset)
WIN11_VM_DISPLAY QEMU display with --window (default: cocoa on macOS, WIN11_VM_DISPLAY QEMU window display (default: cocoa on macOS,
gtk elsewhere) gtk elsewhere)
BASE_BUILD_TMPDIR Parent of the scratch state directory, which needs about BASE_BUILD_TMPDIR Parent of the scratch state directory, which needs about
25 GB (default: /var/tmp) 25 GB (default: /var/tmp)
@@ -75,7 +81,7 @@ force=0
keep_work=0 keep_work=0
compress=0 compress=0
from_img=0 from_img=0
window=0 window=1
output="" output=""
while (($#)); do while (($#)); do
case "$1" in case "$1" in
@@ -84,7 +90,7 @@ while (($#)); do
--keep-work) keep_work=1 ;; --keep-work) keep_work=1 ;;
--compress) compress=1 ;; --compress) compress=1 ;;
--from-img) from_img=1 ;; --from-img) from_img=1 ;;
--window) window=1 ;; --headless) window=0 ;;
-*) usage >&2; exit 2 ;; -*) usage >&2; exit 2 ;;
*) [[ -z "$output" ]] || { usage >&2; exit 2; }; output="$1" ;; *) [[ -z "$output" ]] || { usage >&2; exit 2; }; output="$1" ;;
esac esac
@@ -116,6 +122,22 @@ if ((!from_img)); then
|| die "install socat or nc (it presses a key at the boot prompt of the ISO)." || die "install socat or nc (it presses a key at the boot prompt of the ISO)."
fi fi
# Without the ISO, offer to download it (needs a terminal).
if [[ -z "${WIN11_VM_INSTALL_ISO:-}" ]] && ! "$downloader" --print-path windows11 --iso >/dev/null 2>&1; then
"$downloader" --print-path windows11 --iso >&2 || true
if { : </dev/tty >/dev/tty; } 2>/dev/null; then
printf 'Download the Windows 11 ISO now? [Y/n] ' >/dev/tty
read -r answer </dev/tty || answer=
[[ -z "$answer" || "$answer" == [yY] || "$answer" == [yY][eE][sS] ]] || exit 1
"$downloader" windows11 || exit 1
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)}" output="${output:-$("$downloader" --print-path windows11)}"
if [[ -e "$output" && $force -eq 0 ]]; then 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." die "${output} exists (it may be the backing file of existing VM disks); use --force or another OUTPUT."
@@ -207,6 +229,7 @@ seed_dir="${work}/seed"
mkdir -p "$seed_dir" mkdir -p "$seed_dir"
sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${arch}\"/" \ sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${arch}\"/" \
"${win_dir}/seed/Autounattend.xml" >"${seed_dir}/Autounattend.xml" "${win_dir}/seed/Autounattend.xml" >"${seed_dir}/Autounattend.xml"
cp -- "${win_dir}/seed/Install-VirtIO-Drivers.ps1" "${seed_dir}/"
seed_iso="${work}/seed.iso" seed_iso="${work}/seed.iso"
case "$iso_tool" in case "$iso_tool" in
xorriso) xorriso -as mkisofs -quiet -o "$seed_iso" -V ANSWER -J -r "$seed_dir" ;; xorriso) xorriso -as mkisofs -quiet -o "$seed_iso" -V ANSWER -J -r "$seed_dir" ;;
@@ -227,13 +250,15 @@ config_text="$(<"$config_template")"
for placeholder in uefi_code uefi_vars disk_image monitor_socket; do for placeholder in uefi_code uefi_vars disk_image monitor_socket; do
value="${!placeholder}" value="${!placeholder}"
[[ "$value" != *\"* ]] || die "${placeholder} must not contain double quotes: ${value}" [[ "$value" != *\"* ]] || die "${placeholder} must not contain double quotes: ${value}"
config_text="${config_text//"\${${placeholder}}"/"$value"}" # No outer double quotes: bash 3.2 (macOS) keeps inner quotes literally.
config_text=${config_text//"\${${placeholder}}"/"$value"}
done done
printf '%s\n' "$config_text" >"$qemu_config" printf '%s\n' "$config_text" >"$qemu_config"
if ((window)) && [[ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" && "$host_os" != darwin ]]; then
window=0
fi
if ((window)); then if ((window)); then
[[ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" || "$host_os" == darwin ]] \
|| die "--window needs a desktop session (DISPLAY or WAYLAND_DISPLAY is not set)."
if [[ -n "${WIN11_VM_DISPLAY:-}" ]]; then display="$WIN11_VM_DISPLAY" if [[ -n "${WIN11_VM_DISPLAY:-}" ]]; then display="$WIN11_VM_DISPLAY"
elif [[ "$host_os" == darwin ]]; then display=cocoa elif [[ "$host_os" == darwin ]]; then display=cocoa
else display=gtk; fi else display=gtk; fi
@@ -250,8 +275,11 @@ qemu_args=(
-netdev user,id=net0 -netdev user,id=net0
-nic none -nic none
-display "$display" -display "$display"
-serial "file:${work}/console.log" -daemonize -pidfile "$pid_file" -serial "file:${work}/console.log" -pidfile "$pid_file"
) )
# The Cocoa/GTK window needs the original process (macOS crashes in fork()), so
# only the VNC display is daemonized; the window runs as a background job.
((window)) || qemu_args+=(-daemonize)
if [[ "$host_os" == darwin && "$arch" == arm64 ]] && ((!from_img)); then if [[ "$host_os" == darwin && "$arch" == arm64 ]] && ((!from_img)); then
# With the HVF in-kernel GICv3, QEMU's usb-storage races on the status packet # With the HVF in-kernel GICv3, QEMU's usb-storage races on the status packet
# and Windows Setup stalls 10 s after every burst of reads (utmapp/UTM#7970), # and Windows Setup stalls 10 s after every burst of reads (utmapp/UTM#7970),
@@ -279,6 +307,8 @@ else
attach_media install_media "$install_iso" 2 ide.1 attach_media install_media "$install_iso" 2 ide.1
fi fi
attach_media answer_media "$seed_iso" 3 ide.2 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 # 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 # to boot from CD or DVD...") and gives up after a few seconds. Nobody is there
@@ -295,7 +325,16 @@ press_key() {
fi fi
} }
"$qemu_binary" "${qemu_args[@]}" if ((window)); then
"$qemu_binary" "${qemu_args[@]}" </dev/null &
qemu_job=$!
until [[ -s "$pid_file" ]]; do
kill -0 "$qemu_job" 2>/dev/null || die "QEMU exited before it started the guest."
sleep 0.2
done
else
"$qemu_binary" "${qemu_args[@]}"
fi
if ((!from_img)); then if ((!from_img)); then
( (
deadline=$((SECONDS + 30)) deadline=$((SECONDS + 30))
+87 -14
View File
@@ -19,9 +19,12 @@ if [[ "$image_arch" == arm64 ]]; then
debian_image="${base_qcow2_dir}/debian-13-generic-arm64-20260810-2566.qcow2" debian_image="${base_qcow2_dir}/debian-13-generic-arm64-20260810-2566.qcow2"
alma_image="${base_qcow2_dir}/AlmaLinux-10-GenericCloud-10.2-20260526.0.aarch64.qcow2" alma_image="${base_qcow2_dir}/AlmaLinux-10-GenericCloud-10.2-20260526.0.aarch64.qcow2"
fedora_image="${base_qcow2_dir}/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2" fedora_image="${base_qcow2_dir}/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2"
rocky_image="${base_qcow2_dir}/Rocky-9-GenericCloud-Base-9.8-20260525.0.aarch64.qcow2"
ubuntu_image="${base_qcow2_dir}/ubuntu-26.04-server-cloudimg-arm64-20260927.img" ubuntu_image="${base_qcow2_dir}/ubuntu-26.04-server-cloudimg-arm64-20260927.img"
debian_url_default="https://cloud.debian.org/images/cloud/trixie/20260810-2566/debian-13-generic-arm64-20260810-2566.qcow2" debian_url_default="https://cloud.debian.org/images/cloud/trixie/20260810-2566/debian-13-generic-arm64-20260810-2566.qcow2"
alma_url_default="https://repo.almalinux.org/almalinux/10/cloud/aarch64/images/AlmaLinux-10-GenericCloud-10.2-20260526.0.aarch64.qcow2" alma_url_default="https://repo.almalinux.org/almalinux/10/cloud/aarch64/images/AlmaLinux-10-GenericCloud-10.2-20260526.0.aarch64.qcow2"
rocky_url_default="https://dl.rockylinux.org/pub/rocky/9/images/aarch64/Rocky-9-GenericCloud-Base-9.8-20260525.0.aarch64.qcow2
https://mirrors.nju.edu.cn/rocky/9/images/aarch64/Rocky-9-GenericCloud-Base-9.8-20260525.0.aarch64.qcow2"
fedora_url_default="https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/44/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2 fedora_url_default="https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/44/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2
https://mirrors.bfsu.edu.cn/fedora/releases/44/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2 https://mirrors.bfsu.edu.cn/fedora/releases/44/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2
https://mirrors.nju.edu.cn/fedora/releases/44/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2" https://mirrors.nju.edu.cn/fedora/releases/44/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-44-1.7.aarch64.qcow2"
@@ -35,14 +38,18 @@ if [[ "$image_arch" == arm64 ]]; then
# ALMA_IMAGE_SHA256 / FEDORA_IMAGE_SHA256. # ALMA_IMAGE_SHA256 / FEDORA_IMAGE_SHA256.
debian_sha512_default="" debian_sha512_default=""
alma_sha256_default="" alma_sha256_default=""
rocky_sha256_default="24692a444f1f0b8bb95375c38c8b43f8099a115347623691be2c330b40c8a1fe"
fedora_sha256_default="" fedora_sha256_default=""
else else
debian_image="${base_qcow2_dir}/debian-13-generic-amd64-20260810-2566.qcow2" debian_image="${base_qcow2_dir}/debian-13-generic-amd64-20260810-2566.qcow2"
alma_image="${base_qcow2_dir}/AlmaLinux-10-GenericCloud-10.2-20260526.0.x86_64.qcow2" alma_image="${base_qcow2_dir}/AlmaLinux-10-GenericCloud-10.2-20260526.0.x86_64.qcow2"
fedora_image="${base_qcow2_dir}/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2" fedora_image="${base_qcow2_dir}/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2"
rocky_image="${base_qcow2_dir}/Rocky-9-GenericCloud-Base-9.8-20260525.0.x86_64.qcow2"
ubuntu_image="${base_qcow2_dir}/ubuntu-26.04-server-cloudimg-amd64-20260927.img" ubuntu_image="${base_qcow2_dir}/ubuntu-26.04-server-cloudimg-amd64-20260927.img"
debian_url_default="https://cloud.debian.org/images/cloud/trixie/20260810-2566/debian-13-generic-amd64-20260810-2566.qcow2" debian_url_default="https://cloud.debian.org/images/cloud/trixie/20260810-2566/debian-13-generic-amd64-20260810-2566.qcow2"
alma_url_default="https://repo.almalinux.org/almalinux/10/cloud/x86_64/images/AlmaLinux-10-GenericCloud-10.2-20260526.0.x86_64.qcow2" alma_url_default="https://repo.almalinux.org/almalinux/10/cloud/x86_64/images/AlmaLinux-10-GenericCloud-10.2-20260526.0.x86_64.qcow2"
rocky_url_default="https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base-9.8-20260525.0.x86_64.qcow2
https://mirrors.nju.edu.cn/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base-9.8-20260525.0.x86_64.qcow2"
fedora_url_default="https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2 fedora_url_default="https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2
https://mirrors.bfsu.edu.cn/fedora/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2 https://mirrors.bfsu.edu.cn/fedora/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2
https://mirrors.nju.edu.cn/fedora/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2" https://mirrors.nju.edu.cn/fedora/releases/44/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-44-1.7.x86_64.qcow2"
@@ -51,6 +58,7 @@ else
https://cloud-images.ubuntu.com/releases/26.04/release-20260927/ubuntu-26.04-server-cloudimg-amd64.img" https://cloud-images.ubuntu.com/releases/26.04/release-20260927/ubuntu-26.04-server-cloudimg-amd64.img"
debian_sha512_default="f6978100d8031c266d55d7815ceea7fcdeacf28e1e5834fdb9c94ac96880a054a6e6f8681c2d3b0584e0057eaf3ef7353856b85212d04134744faa9b3bb1f24f" debian_sha512_default="f6978100d8031c266d55d7815ceea7fcdeacf28e1e5834fdb9c94ac96880a054a6e6f8681c2d3b0584e0057eaf3ef7353856b85212d04134744faa9b3bb1f24f"
alma_sha256_default="47f2218668dd4776be140dd92fa3bea700be1766e2c7d88bdfd6a4b50f477b4d" alma_sha256_default="47f2218668dd4776be140dd92fa3bea700be1766e2c7d88bdfd6a4b50f477b4d"
rocky_sha256_default="92c206cc6f790c61583247eefe87890f8828420662c17cacf247cec78ab4eec8"
fedora_sha256_default="28680fe5b371a5a82ebf43a31926e086a168e59949d03969c5093e7071f90b7f" fedora_sha256_default="28680fe5b371a5a82ebf43a31926e086a168e59949d03969c5093e7071f90b7f"
ubuntu_sha256_default="8800651811af9a85465ad1d552add729947bb16488dddb4a9b5305a3d97332b2" ubuntu_sha256_default="8800651811af9a85465ad1d552add729947bb16488dddb4a9b5305a3d97332b2"
fi fi
@@ -107,8 +115,10 @@ windows_qcow2_path() {
iso="$(basename -- "$iso")" iso="$(basename -- "$iso")"
printf '%s/%s.qcow2\n' "$base_qcow2_dir" "${iso%.*}" printf '%s/%s.qcow2\n' "$base_qcow2_dir" "${iso%.*}"
elif ! find_windows_image "$base_qcow2_dir" qcow2; then elif ! find_windows_image "$base_qcow2_dir" qcow2; then
printf 'Error: no Windows 11 %s ISO (*Win*11*%s*.iso) found in %s\n' \ printf 'Error: no Windows 11 %s base QCOW2 image (*Win*11*%s*.qcow2) found in %s\n' \
"$windows_arch" "$windows_arch" "$install_iso_dir" >&2 "$windows_arch" "$windows_arch" "$base_qcow2_dir" >&2
printf 'Copy a base image there, set WIN11_VM_BASE_IMAGE, or build one from an ISO in %s with build-windows-base.sh.\n' \
"$install_iso_dir" >&2
return 1 return 1
fi fi
} }
@@ -118,27 +128,29 @@ windows_qcow2_path() {
# them in order. # them in order.
debian_url="${DEBIAN_IMAGE_URL:-$debian_url_default}" debian_url="${DEBIAN_IMAGE_URL:-$debian_url_default}"
alma_url="${ALMA_IMAGE_URL:-$alma_url_default}" alma_url="${ALMA_IMAGE_URL:-$alma_url_default}"
rocky_url="${ROCKY_IMAGE_URL:-$rocky_url_default}"
fedora_url="${FEDORA_IMAGE_URL:-$fedora_url_default}" fedora_url="${FEDORA_IMAGE_URL:-$fedora_url_default}"
ubuntu_url="${UBUNTU_IMAGE_URL:-$ubuntu_url_default}" ubuntu_url="${UBUNTU_IMAGE_URL:-$ubuntu_url_default}"
openssh_url="${OPENSSH_WIN_ZIP_URL:-https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/${openssh_zip}}" openssh_url="${OPENSSH_WIN_ZIP_URL:-https://wangguofang.net/github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/${openssh_zip}}"
virtio_url="${VIRTIO_WIN_ISO_URL:-https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso}" virtio_url="${VIRTIO_WIN_ISO_URL:-https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso}"
debian_sha512="${DEBIAN_IMAGE_SHA512:-$debian_sha512_default}" debian_sha512="${DEBIAN_IMAGE_SHA512:-$debian_sha512_default}"
alma_sha256="${ALMA_IMAGE_SHA256:-$alma_sha256_default}" alma_sha256="${ALMA_IMAGE_SHA256:-$alma_sha256_default}"
rocky_sha256="${ROCKY_IMAGE_SHA256:-$rocky_sha256_default}"
fedora_sha256="${FEDORA_IMAGE_SHA256:-$fedora_sha256_default}" fedora_sha256="${FEDORA_IMAGE_SHA256:-$fedora_sha256_default}"
ubuntu_sha256="${UBUNTU_IMAGE_SHA256:-$ubuntu_sha256_default}" ubuntu_sha256="${UBUNTU_IMAGE_SHA256:-$ubuntu_sha256_default}"
force_download=false force_download=false
usage() { usage() {
cat <<EOF cat <<EOF
Usage: $(basename "$0") [--force] [all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win|openssh-win|windows11] ... Usage: $(basename "$0") [--force] [all|debian13|almalinux10|rocky9|fedora44|ubuntu2604|virtio-win|openssh-win|windows11] ...
$(basename "$0") --print-path TARGET [--iso] $(basename "$0") --print-path TARGET [--iso]
Download one or more shared VM images. With no arguments, all targets are Download one or more shared VM images. With no arguments, all targets are
processed. The Debian/AlmaLinux/Fedora/Ubuntu image architecture (arm64 or amd64) processed. The Debian/AlmaLinux/Rocky Linux/Fedora/Ubuntu image architecture (arm64 or amd64)
is auto-detected from the host; override it with VM_IMAGE_ARCH. is auto-detected from the host; override it with VM_IMAGE_ARCH.
When aria2c is installed and an image has several mirrors, the image is When aria2c is installed, an image is downloaded with 16 connections in total
downloaded from all of them at once, with one connection per mirror. (DOWNLOAD_CONNECTIONS, up to 16), spread across all its mirrors if it has several.
Otherwise curl or wget downloads from the mirrors in order. Otherwise curl or wget downloads from the mirrors in order.
The windows11 target is not part of "all". It downloads the Windows 11 ISO for The windows11 target is not part of "all". It downloads the Windows 11 ISO for
@@ -154,6 +166,36 @@ The Windows ISO is found automatically in install-iso/ by matching
EOF EOF
} }
# Ask a yes/no question on the terminal; fails when there is none or the answer
# is not yes (an empty answer means yes). stdout carries the printed path, so all dialogue goes to /dev/tty.
ask_yes_no() {
local answer
{ : </dev/tty >/dev/tty; } 2>/dev/null || return 1
printf '%s [Y/n] ' "$1" >/dev/tty
read -r answer </dev/tty || return 1
[[ -z "$answer" || "$answer" == [yY] || "$answer" == [yY][eE][sS] ]]
}
# Print the base QCOW2 path. When the image does not exist and a terminal is
# attached, offer to run build-windows-base.sh, which in turn offers to download
# the ISO. The builder sets VM_IMAGE_NO_PROMPT so its own lookups never ask.
windows_base_path() {
local qcow2 builder="${images_dir}/build-windows-base.sh"
if qcow2="$(windows_qcow2_path 2>/dev/null)" && [[ -f "$qcow2" ]]; then
printf '%s\n' "$qcow2"
return 0
fi
windows_qcow2_path >/dev/null || true
if [[ -z "${VM_IMAGE_NO_PROMPT:-}" && -x "$builder" ]] && ask_yes_no "Build the Windows 11 base image now with build-windows-base.sh?"; then
# stdout carries the path, so the build's output goes to the terminal.
"$builder" >/dev/tty 2>&1 </dev/tty || return 1
windows_qcow2_path
return
fi
# Name the expected path if the ISO fixes it; the caller reports it missing.
windows_qcow2_path 2>/dev/null
}
print_image_path() { print_image_path() {
local target="$1" local target="$1"
local media="${2:-qcow2}" local media="${2:-qcow2}"
@@ -161,6 +203,7 @@ print_image_path() {
case "$target" in case "$target" in
debian13) printf '%s\n' "$debian_image" ;; debian13) printf '%s\n' "$debian_image" ;;
almalinux10) printf '%s\n' "$alma_image" ;; almalinux10) printf '%s\n' "$alma_image" ;;
rocky9) printf '%s\n' "$rocky_image" ;;
fedora44) printf '%s\n' "$fedora_image" ;; fedora44) printf '%s\n' "$fedora_image" ;;
ubuntu2604) printf '%s\n' "$ubuntu_image" ;; ubuntu2604) printf '%s\n' "$ubuntu_image" ;;
virtio-win) printf '%s\n' "$virtio_image" ;; virtio-win) printf '%s\n' "$virtio_image" ;;
@@ -169,7 +212,7 @@ print_image_path() {
if [[ "$media" == iso ]]; then if [[ "$media" == iso ]]; then
windows_iso_path windows_iso_path
else else
windows_qcow2_path windows_base_path
fi fi
;; ;;
*) *)
@@ -210,7 +253,7 @@ fi
for target in "${targets[@]}"; do for target in "${targets[@]}"; do
case "$target" in case "$target" in
all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win|openssh-win|windows11) ;; all|debian13|almalinux10|rocky9|fedora44|ubuntu2604|virtio-win|openssh-win|windows11) ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) printf 'Error: unknown download target: %s\n' "$target" >&2; usage >&2; exit 2 ;; *) printf 'Error: unknown download target: %s\n' "$target" >&2; usage >&2; exit 2 ;;
esac esac
@@ -273,16 +316,17 @@ download_single_source() {
fi fi
} }
# Download to $partial with aria2c, splitting the file across all URLs with a # Download to $partial with aria2c, splitting the file across all URLs with up
# single connection to each server. # to DOWNLOAD_CONNECTIONS (default 16) connections in total and to each server.
download_multi_source() { download_multi_source() {
local partial="$1" local partial="$1"
shift shift
local connections="${DOWNLOAD_CONNECTIONS:-16}"
aria2c \ aria2c \
--continue=true \ --continue=true \
--split="$#" \ --split="$connections" \
--max-connection-per-server=1 \ --max-connection-per-server="$connections" \
--file-allocation=none \ --file-allocation=none \
--auto-file-renaming=false \ --auto-file-renaming=false \
--max-tries=5 \ --max-tries=5 \
@@ -414,7 +458,27 @@ download_windows_iso() {
printf 'Error: node is required to get the Windows 11 ISO URL.\n' >&2 printf 'Error: node is required to get the Windows 11 ISO URL.\n' >&2
exit 1 exit 1
fi fi
url="$(node "${images_dir}/nodejs/get-windows-iso-url.js" --arch "$windows_arch")" if [[ ! -d "${images_dir}/nodejs/node_modules/playwright-core" ]]; then
printf 'Error: the Node.js dependencies are not installed (npm ci has not been run in %s/nodejs).\n' \
"$images_dir" >&2
command -v npm >/dev/null 2>&1 || { printf 'Error: npm is required; install Node.js with npm.\n' >&2; exit 1; }
ask_yes_no "Run npm ci in ${images_dir}/nodejs now?" || exit 1
(cd "${images_dir}/nodejs" && npm ci) || exit 1
fi
local status=0
url="$(node "${images_dir}/nodejs/get-windows-iso-url.js" --arch "$windows_arch")" || status=$?
if ((status == 2)) && [[ -z "${CHROME_PATH:-}" ]]; then
# Exit status 2 means Chromium was not found (the script has said so).
ask_yes_no "Install Chromium with playwright-core now?" || exit 1
# Default to the npmmirror mirror (the official CDN is slow in China); set
# PLAYWRIGHT_DOWNLOAD_HOST to override it.
(cd "${images_dir}/nodejs" \
&& PLAYWRIGHT_DOWNLOAD_HOST="${PLAYWRIGHT_DOWNLOAD_HOST:-https://cdn.npmmirror.com/binaries/playwright}" \
npx playwright-core install chromium) || exit 1
url="$(node "${images_dir}/nodejs/get-windows-iso-url.js" --arch "$windows_arch")" || exit 1
elif ((status != 0)); then
exit "$status"
fi
[[ -n "$url" ]] || { printf 'Error: no Windows 11 ISO URL.\n' >&2; exit 1; } [[ -n "$url" ]] || { printf 'Error: no Windows 11 ISO URL.\n' >&2; exit 1; }
name="${url%%\?*}" name="${url%%\?*}"
name="${name##*/}" name="${name##*/}"
@@ -446,6 +510,15 @@ if wants_target almalinux10; then
"$alma_sha256" "$alma_sha256"
fi fi
if wants_target rocky9; then
download_file \
'Rocky Linux 9 cloud image' \
"$rocky_url" \
"$rocky_image" \
sha256 \
"$rocky_sha256"
fi
if wants_target fedora44; then if wants_target fedora44; then
download_file \ download_file \
'Fedora 44 cloud image' \ 'Fedora 44 cloud image' \
+12 -11
View File
@@ -5,12 +5,11 @@
// Usage: node get-windows-iso-url.js [--arch x64|arm64] [--lang NAME] // Usage: node get-windows-iso-url.js [--arch x64|arm64] [--lang NAME]
// Environment: // Environment:
// PLAYWRIGHT_CORE playwright-core directory (default: node_modules here) // PLAYWRIGHT_CORE playwright-core directory (default: node_modules here)
// CHROME_PATH Chromium executable (default: newest one in ~/.cache/ms-playwright) // CHROME_PATH Chromium executable (default: the one Playwright installed)
// The URL expires after about 24 hours and may be bound to the IP that // The URL expires after about 24 hours and may be bound to the IP that
// requested it. Only the URL goes to stdout; logs go to stderr. // requested it. Only the URL goes to stdout; logs go to stderr.
const fs = require('fs'); const fs = require('fs');
const os = require('os');
const path = require('path'); const path = require('path');
const args = process.argv.slice(2); const args = process.argv.slice(2);
@@ -28,8 +27,7 @@ Options:
Environment: Environment:
PLAYWRIGHT_CORE playwright-core directory (default: node_modules here) PLAYWRIGHT_CORE playwright-core directory (default: node_modules here)
CHROME_PATH Chromium executable (default: newest one in CHROME_PATH Chromium executable (default: the one Playwright installed)
~/.cache/ms-playwright)
Examples: Examples:
node get-windows-iso-url.js --arch x64 --lang "Chinese Simplified" node get-windows-iso-url.js --arch x64 --lang "Chinese Simplified"
@@ -60,17 +58,20 @@ function findPlaywright() {
process.exit(2); process.exit(2);
} }
function findChrome() { // CHROME_PATH wins; otherwise Playwright picks the Chromium it installed (in
if (process.env.CHROME_PATH) return process.env.CHROME_PATH; // its own per-OS cache, PLAYWRIGHT_BROWSERS_PATH if set).
const root = path.join(os.homedir(), '.cache/ms-playwright'); function launchOptions(chromium) {
const dirs = fs.existsSync(root) ? fs.readdirSync(root).filter(d => /^chromium-\d+$/.test(d)).sort() : []; if (process.env.CHROME_PATH) return { executablePath: process.env.CHROME_PATH, headless: true };
if (!dirs.length) { console.error('Chromium not found: set CHROME_PATH or run npx playwright-core install chromium'); process.exit(2); } if (!fs.existsSync(chromium.executablePath())) {
return path.join(root, dirs.pop(), 'chrome-linux64/chrome'); console.error('Chromium not found: set CHROME_PATH or run npx playwright-core install chromium');
process.exit(2);
}
return { headless: true };
} }
(async () => { (async () => {
const { chromium } = require(findPlaywright()); const { chromium } = require(findPlaywright());
const browser = await chromium.launch({ executablePath: findChrome(), headless: true }); const browser = await chromium.launch(launchOptions(chromium));
try { try {
const ctx = await browser.newContext({ const ctx = await browser.newContext({
locale: 'en-US', locale: 'en-US',
+5
View File
@@ -131,6 +131,11 @@
</RunSynchronousCommand> </RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add"> <RunSynchronousCommand wcm:action="add">
<Order>4</Order> <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> <Description>Generalize the image and power off</Description>
<Path>C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /shutdown /quiet</Path> <Path>C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /shutdown /quiet</Path>
</RunSynchronousCommand> </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."