Compare commits
21
Commits
21604f511f
...
main
No files matched your search
@@ -4,10 +4,15 @@
|
||||
*.img
|
||||
*.vhd
|
||||
*.vhdx
|
||||
*.zip
|
||||
*.part
|
||||
*.part.url
|
||||
*.aria2
|
||||
|
||||
# Keep the intended directory layout in Git.
|
||||
!base-qcow2/.gitkeep
|
||||
!install-iso/.gitkeep
|
||||
!tools-iso/.gitkeep
|
||||
|
||||
# Node dependencies of nodejs/get-windows-iso-url.js.
|
||||
node_modules/
|
||||
@@ -17,6 +17,7 @@ Download or check selected targets:
|
||||
```bash
|
||||
./download-images.sh debian13
|
||||
./download-images.sh almalinux10
|
||||
./download-images.sh rocky9
|
||||
./download-images.sh fedora44
|
||||
./download-images.sh ubuntu2604
|
||||
./download-images.sh virtio-win
|
||||
@@ -30,6 +31,26 @@ arm64 hosts) and `.iso` in that order (`*Win*11*x64*.iso` or
|
||||
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:
|
||||
@@ -37,6 +58,7 @@ 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 rocky9
|
||||
./download-images.sh --print-path fedora44
|
||||
./download-images.sh --print-path ubuntu2604
|
||||
./download-images.sh --print-path virtio-win
|
||||
@@ -51,17 +73,23 @@ 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.
|
||||
|
||||
Download URLs can be overridden with `DEBIAN_IMAGE_URL`, `ALMA_IMAGE_URL`,
|
||||
`FEDORA_IMAGE_URL`, `UBUNTU_IMAGE_URL`, and `VIRTIO_WIN_ISO_URL`. Each may
|
||||
`ROCKY_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
|
||||
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.
|
||||
|
||||
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.
|
||||
it starts over. The Rocky Linux, Fedora and Ubuntu checksums can be overridden with
|
||||
`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
|
||||
architecture into `tools-iso/`: `OpenSSH-Win64.zip` on amd64 hosts and
|
||||
`OpenSSH-ARM64.zip` on arm64 hosts.
|
||||
|
||||
## Windows installation disk image
|
||||
|
||||
@@ -79,8 +107,37 @@ 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 `.install.img` in place of `.iso`. The
|
||||
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.
|
||||
|
||||
```bash
|
||||
./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).
|
||||
Executable
+369
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a clean, generic Windows 11 base QCOW2 image.
|
||||
#
|
||||
# A VM disk can then be created as a QCOW2 overlay on this image, so a new VM
|
||||
# starts in seconds instead of running Windows Setup again. The base image is
|
||||
# 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 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 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
|
||||
# VM is configured on its first boot: Windows looks for an Unattend.xml in the
|
||||
# root of an attached CD-ROM during its specialize and OOBE passes, and so
|
||||
# takes the host name, account, auto logon, SSH and so on from a seed ISO.
|
||||
#
|
||||
# Needs qemu (qemu-system-x86_64 or qemu-system-aarch64), qemu-img, an ISO tool
|
||||
# (xorriso, genisoimage or mkisofs), socat or nc (to press a key at the boot
|
||||
# prompt of the ISO), the UEFI firmware for QEMU and the Windows ISO in
|
||||
# install-iso/ (see README.md).
|
||||
#
|
||||
# Usage: build-windows-base.sh [--force] [--keep-work] [--compress] [--from-img]
|
||||
# [--headless] [OUTPUT.qcow2]
|
||||
# OUTPUT defaults to the path that "download-images.sh --print-path windows11"
|
||||
# prints.
|
||||
set -euo pipefail
|
||||
|
||||
images_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
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"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [--force] [--keep-work] [--compress] [--from-img]
|
||||
[--headless] [OUTPUT.qcow2]
|
||||
|
||||
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.
|
||||
|
||||
--force Replace OUTPUT if it exists
|
||||
--keep-work Keep the scratch state directory (also kept on failure)
|
||||
--compress Compress the output (smaller, but clones read it slower)
|
||||
--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 emulated CD-ROM
|
||||
--headless Serve the guest screen over VNC on 127.0.0.1:5990 instead of
|
||||
showing a QEMU window (the default; without a desktop
|
||||
session on Linux, VNC is used as well)
|
||||
-h, --help Show this help
|
||||
|
||||
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)
|
||||
WIN11_VM_DISPLAY QEMU window display (default: cocoa on macOS,
|
||||
gtk elsewhere)
|
||||
BASE_BUILD_TMPDIR Parent of the scratch state directory, which needs about
|
||||
25 GB (default: /var/tmp)
|
||||
BASE_BUILD_TIMEOUT Seconds to wait for installation and sysprep (default: 5400)
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Error: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
force=0
|
||||
keep_work=0
|
||||
compress=0
|
||||
from_img=0
|
||||
window=1
|
||||
output=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--force) force=1 ;;
|
||||
--keep-work) keep_work=1 ;;
|
||||
--compress) compress=1 ;;
|
||||
--from-img) from_img=1 ;;
|
||||
--headless) window=0 ;;
|
||||
-*) usage >&2; exit 2 ;;
|
||||
*) [[ -z "$output" ]] || { usage >&2; exit 2; }; output="$1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
case "${VM_IMAGE_ARCH:-$(uname -m)}" in
|
||||
arm64|aarch64) arch=arm64 ;;
|
||||
x86_64|amd64) arch=amd64 ;;
|
||||
*) die "unsupported architecture: ${VM_IMAGE_ARCH:-$(uname -m)}" ;;
|
||||
esac
|
||||
export VM_IMAGE_ARCH="$arch"
|
||||
host_os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
[[ "$host_os" != darwin || "$arch" == arm64 ]] \
|
||||
|| die "amd64 guests are not supported on macOS hosts; use an arm64 image"
|
||||
if [[ "$arch" == arm64 ]]; then qemu_binary=qemu-system-aarch64; else qemu_binary=qemu-system-x86_64; fi
|
||||
|
||||
for tool in "$qemu_binary" qemu-img; do
|
||||
command -v "$tool" >/dev/null || die "${tool} is not installed."
|
||||
done
|
||||
[[ -x "$downloader" ]] || die "download-images.sh was not found: ${downloader}"
|
||||
iso_tool=""
|
||||
for tool in xorriso genisoimage mkisofs; do
|
||||
command -v "$tool" >/dev/null && { iso_tool="$tool"; break; }
|
||||
done
|
||||
[[ -n "$iso_tool" ]] || die "install xorriso, genisoimage, or mkisofs."
|
||||
if ((!from_img)); then
|
||||
command -v socat >/dev/null || command -v nc >/dev/null \
|
||||
|| die "install socat or nc (it presses a key at the boot prompt of the ISO)."
|
||||
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)}"
|
||||
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."
|
||||
fi
|
||||
|
||||
install_iso="${WIN11_VM_INSTALL_ISO:-$("$downloader" --print-path windows11 --iso)}"
|
||||
((!from_img)) || install_iso="${install_iso%.*}.img"
|
||||
[[ -f "$install_iso" ]] || die "the Windows 11 installation medium was not found: ${install_iso} (see README.md)."
|
||||
|
||||
pick_firmware() {
|
||||
local override="$1" candidate
|
||||
shift
|
||||
if [[ -n "$override" ]]; then printf '%s\n' "$override"; return; fi
|
||||
for candidate in "$@"; do
|
||||
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return; }
|
||||
done
|
||||
printf '%s\n' "$1"
|
||||
}
|
||||
if [[ "$arch" == arm64 ]]; then
|
||||
uefi_code="$(pick_firmware "${WIN11_VM_UEFI_CODE:-}" \
|
||||
/usr/share/AAVMF/AAVMF_CODE.fd /usr/share/edk2/aarch64/QEMU_EFI.fd \
|
||||
/opt/homebrew/share/qemu/edk2-aarch64-code.fd /usr/local/share/qemu/edk2-aarch64-code.fd)"
|
||||
uefi_vars_template="$(pick_firmware "${WIN11_VM_UEFI_VARS:-}" \
|
||||
/usr/share/AAVMF/AAVMF_VARS.fd /usr/share/edk2/aarch64/vars-template-pflash.raw \
|
||||
/opt/homebrew/share/qemu/edk2-arm-vars.fd /usr/local/share/qemu/edk2-arm-vars.fd)"
|
||||
else
|
||||
uefi_code="$(pick_firmware "${WIN11_VM_UEFI_CODE:-}" \
|
||||
/usr/share/OVMF/OVMF_CODE_4M.fd /usr/share/edk2/ovmf/OVMF_CODE.fd \
|
||||
/opt/homebrew/share/qemu/edk2-x86_64-code.fd /usr/local/share/qemu/edk2-x86_64-code.fd)"
|
||||
uefi_vars_template="$(pick_firmware "${WIN11_VM_UEFI_VARS:-}" \
|
||||
/usr/share/OVMF/OVMF_VARS_4M.fd /usr/share/edk2/ovmf/OVMF_VARS.fd \
|
||||
/opt/homebrew/share/qemu/edk2-i386-vars.fd /usr/local/share/qemu/edk2-i386-vars.fd)"
|
||||
fi
|
||||
config_template="${win_dir}/qemu/config.ini"
|
||||
platform_config="${win_dir}/qemu/${host_os}-${arch}.ini"
|
||||
for file in "$uefi_code" "$uefi_vars_template" "$config_template" "$platform_config" \
|
||||
"${win_dir}/seed/Autounattend.xml"; do
|
||||
[[ -f "$file" ]] || die "required file was not found: ${file}"
|
||||
done
|
||||
|
||||
total_steps=3
|
||||
step_number=0
|
||||
step() {
|
||||
step_number=$((step_number + 1))
|
||||
printf '[%d/%d] %s\n' "$step_number" "$total_steps" "$*"
|
||||
}
|
||||
note() {
|
||||
printf ' %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
mkdir -p "${BASE_BUILD_TMPDIR:-/var/tmp}"
|
||||
work="$(mktemp -d "${BASE_BUILD_TMPDIR:-/var/tmp}/build-windows-base.XXXXXX")"
|
||||
pid_file="${work}/qemu.pid"
|
||||
monitor_socket="${work}/qemu-monitor.sock"
|
||||
succeeded=0
|
||||
|
||||
vm_running() {
|
||||
[[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if vm_running; then
|
||||
kill "$(cat "$pid_file")" 2>/dev/null || true
|
||||
fi
|
||||
if ((succeeded && !keep_work)); then
|
||||
rm -rf "$work"
|
||||
else
|
||||
note "Scratch directory kept: ${work}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Poll "$@" every 10 s until it succeeds; give up after $1 seconds.
|
||||
wait_for() {
|
||||
local limit="$1"
|
||||
shift
|
||||
local deadline=$((SECONDS + limit))
|
||||
until "$@"; do
|
||||
((SECONDS < deadline)) || return 1
|
||||
sleep 10
|
||||
done
|
||||
}
|
||||
|
||||
# --- 1. The answer media -----------------------------------------------------
|
||||
step "Building the answer ISO"
|
||||
# The answer file's components name the processor architecture, so render a
|
||||
# copy for the target architecture.
|
||||
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" ;;
|
||||
*) "$iso_tool" -quiet -o "$seed_iso" -V ANSWER -J -r "$seed_dir" ;;
|
||||
esac
|
||||
|
||||
# --- 2. The installation -----------------------------------------------------
|
||||
step "Installing and generalizing Windows 11 (unattended; takes a while)"
|
||||
disk_image="${work}/disk.qcow2"
|
||||
uefi_vars="${work}/OVMF_VARS.fd"
|
||||
qemu_config="${work}/qemu.ini"
|
||||
cp -- "$uefi_vars_template" "$uefi_vars"
|
||||
qemu-img create -q -f qcow2 "$disk_image" "${WIN11_VM_DISK_SIZE:-64G}"
|
||||
|
||||
# Fill in the file path placeholders of the config template. QEMU's config file
|
||||
# parser has no escape syntax for quoted values.
|
||||
config_text="$(<"$config_template")"
|
||||
for placeholder in uefi_code uefi_vars disk_image monitor_socket; do
|
||||
value="${!placeholder}"
|
||||
[[ "$value" != *\"* ]] || die "${placeholder} must not contain double quotes: ${value}"
|
||||
# No outer double quotes: bash 3.2 (macOS) keeps inner quotes literally.
|
||||
config_text=${config_text//"\${${placeholder}}"/"$value"}
|
||||
done
|
||||
printf '%s\n' "$config_text" >"$qemu_config"
|
||||
|
||||
if ((window)) && [[ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" && "$host_os" != darwin ]]; then
|
||||
window=0
|
||||
fi
|
||||
if ((window)); then
|
||||
if [[ -n "${WIN11_VM_DISPLAY:-}" ]]; then display="$WIN11_VM_DISPLAY"
|
||||
elif [[ "$host_os" == darwin ]]; then display=cocoa
|
||||
else display=gtk; fi
|
||||
else
|
||||
display="vnc=127.0.0.1:90"
|
||||
fi
|
||||
|
||||
qemu_args=(
|
||||
-readconfig "$qemu_config" -readconfig "$platform_config"
|
||||
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
|
||||
-cpu max
|
||||
# The network that the NIC in the config file uses is defined here; -nic
|
||||
# none stops QEMU from adding a default NIC. Nothing is forwarded.
|
||||
-netdev user,id=net0
|
||||
-nic none
|
||||
-display "$display"
|
||||
-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
|
||||
# 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),
|
||||
# so use the emulated interrupt controller. The NVMe install disk of
|
||||
# --from-img does not use usb-storage, and hangs with the emulated controller.
|
||||
qemu_args+=(-machine kernel-irqchip=off)
|
||||
fi
|
||||
# q35 has IDE for the installation media; "virt" does not, so use USB.
|
||||
attach_media() {
|
||||
local id="$1" file="$2" boot_index="$3" ide_bus="$4"
|
||||
qemu_args+=(-drive "file=${file},if=none,id=${id},media=cdrom,readonly=on")
|
||||
if [[ "$arch" == amd64 ]]; then
|
||||
qemu_args+=(-device "ide-cd,drive=${id},bus=${ide_bus},bootindex=${boot_index}")
|
||||
else
|
||||
qemu_args+=(-device "usb-storage,drive=${id},bootindex=${boot_index}")
|
||||
fi
|
||||
}
|
||||
if ((from_img)); then
|
||||
# snapshot=on: NVMe cannot report write protection, and Windows writes to the
|
||||
# volume it mounts, so a read-only drive makes it fail. The writes go to a
|
||||
# temporary overlay and the image stays unchanged.
|
||||
qemu_args+=(-drive "file=${install_iso},if=none,id=install_media,format=raw,snapshot=on"
|
||||
-device "nvme,drive=install_media,serial=installer,bootindex=2")
|
||||
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
|
||||
# to press it, so a background loop taps the "z" key through the QEMU monitor
|
||||
# for the first 30 s. The key has to produce a character (UEFI ignores a lone
|
||||
# Shift) and Windows Setup starts within that time, so it must not be one that
|
||||
# Setup acts on: Space and Enter press its focused Cancel button, and Y, N and
|
||||
# C are accelerators of its buttons.
|
||||
press_key() {
|
||||
if command -v socat >/dev/null; then
|
||||
printf 'sendkey z\n' | socat -t1 - "UNIX-CONNECT:${monitor_socket}"
|
||||
else
|
||||
printf 'sendkey z\n' | nc -U -w1 "$monitor_socket"
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
(
|
||||
deadline=$((SECONDS + 30))
|
||||
while ((SECONDS < deadline)); do
|
||||
[[ -S "$monitor_socket" ]] && press_key >/dev/null 2>&1 || true
|
||||
sleep 1
|
||||
done
|
||||
) &
|
||||
fi
|
||||
|
||||
# --- 3. Wait for the guest to power off -------------------------------------
|
||||
if ((window)); then
|
||||
note "Screen: QEMU window; console log: ${work}/console.log"
|
||||
else
|
||||
note "Screen: VNC 127.0.0.1:5990; console log: ${work}/console.log"
|
||||
fi
|
||||
guest_off() { ! vm_running; }
|
||||
if ! wait_for "${BASE_BUILD_TIMEOUT:-5400}" guest_off; then
|
||||
die "the guest did not power off in time; look at its screen. If sysprep failed, its log is C:\\Windows\\System32\\Sysprep\\Panther\\setuperr.log in the guest."
|
||||
fi
|
||||
|
||||
step "Converting the disk to ${output}"
|
||||
mkdir -p "$(dirname "$output")"
|
||||
convert_options=(-f qcow2 -O qcow2)
|
||||
((compress)) && convert_options+=(-c)
|
||||
rm -f "${output}.part"
|
||||
qemu-img convert -p "${convert_options[@]}" "${work}/disk.qcow2" "${output}.part"
|
||||
[[ ! -e "$output" ]] || chmod u+w "$output"
|
||||
mv -f "${output}.part" "$output"
|
||||
chmod a-w "$output"
|
||||
succeeded=1
|
||||
printf 'Windows 11 base image is ready: %s\n' "$output"
|
||||
@@ -15,10 +15,10 @@
|
||||
#
|
||||
# Dependencies
|
||||
# macOS (Homebrew) wimlib dosfstools gptfdisk mtools
|
||||
# Debian wimtools dosfstools gdisk mtools udisks2
|
||||
# Debian wimtools dosfstools gdisk mtools rsync udisks2
|
||||
#
|
||||
# Usage: build-windows-install-disk.sh [--force] ISO [OUTPUT.img]
|
||||
# OUTPUT defaults to the ISO path with ".install.img" in place of ".iso".
|
||||
# OUTPUT defaults to the ISO path with ".img" in place of ".iso".
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
@@ -39,7 +39,7 @@ if ((${#args[@]} < 1 || ${#args[@]} > 2)); then
|
||||
exit 2
|
||||
fi
|
||||
iso="${args[0]}"
|
||||
output="${args[1]:-${iso%.*}.install.img}"
|
||||
output="${args[1]:-${iso%.*}.img}"
|
||||
|
||||
die() {
|
||||
printf 'Error: %s\n' "$1" >&2
|
||||
@@ -60,6 +60,18 @@ note() {
|
||||
printf ' %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
# show logs a command line before it runs; run logs it and then runs it. Use show
|
||||
# alone where the output of the command is captured.
|
||||
show() {
|
||||
local line
|
||||
printf -v line '%q ' "$@"
|
||||
note "\$ ${line% }"
|
||||
}
|
||||
run() {
|
||||
show "$@"
|
||||
"$@"
|
||||
}
|
||||
|
||||
os="$(uname -s)"
|
||||
case "$os" in
|
||||
Darwin|Linux) ;;
|
||||
@@ -76,7 +88,7 @@ for dir in ${brew_prefix:+"$brew_prefix/sbin"} /opt/homebrew/sbin /usr/local/sbi
|
||||
[[ -d "$dir" ]] && PATH="$PATH:$dir"
|
||||
done
|
||||
|
||||
tools=(wimlib-imagex mkfs.fat sgdisk mcopy mmd)
|
||||
tools=(wimlib-imagex mkfs.fat sgdisk mcopy rsync)
|
||||
[[ "$os" == Darwin ]] && tools+=(hdiutil)
|
||||
missing=()
|
||||
for tool in "${tools[@]}"; do
|
||||
@@ -86,7 +98,7 @@ if ((${#missing[@]} > 0)); then
|
||||
if [[ "$os" == Darwin ]]; then
|
||||
hint="brew install wimlib dosfstools gptfdisk mtools"
|
||||
else
|
||||
hint="apt install wimtools dosfstools gdisk mtools"
|
||||
hint="apt install wimtools dosfstools gdisk mtools rsync"
|
||||
fi
|
||||
die "missing tools: ${missing[*]} ($hint)"
|
||||
fi
|
||||
@@ -99,19 +111,27 @@ fi
|
||||
# stat prints a file size with -f %z on macOS (BSD) and -c %s on Linux (GNU).
|
||||
if [[ "$os" == Darwin ]]; then size_args=(-f %z); else size_args=(-c %s); fi
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/win11-install-disk.XXXXXX")"
|
||||
# The work directory holds about 10 GiB, so it defaults to /var/tmp: /tmp is
|
||||
# memory-backed on some systems (Debian 13, Fedora, Arch).
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/var/tmp}/win11-install-disk.XXXXXX")"
|
||||
mount_dir="$work_dir/iso"
|
||||
mkdir "$mount_dir"
|
||||
note "work directory: $work_dir"
|
||||
mount_method=""
|
||||
loop_device=""
|
||||
# Set while the output image is being written, so that a failed or cancelled
|
||||
# run does not leave a half-built image behind.
|
||||
partial_output=""
|
||||
cleanup() {
|
||||
[[ -z "$partial_output" ]] || rm -f "$partial_output"
|
||||
case "$mount_method" in
|
||||
hdiutil) hdiutil detach -quiet "$mount_dir" >/dev/null 2>&1 || true ;;
|
||||
udisks)
|
||||
udisksctl unmount -b "$loop_device" >/dev/null 2>&1 || true
|
||||
udisksctl loop-delete -b "$loop_device" >/dev/null 2>&1 || true ;;
|
||||
esac
|
||||
# Staged directories may be read-only when the run was cancelled early.
|
||||
chmod -R u+rwX "$work_dir" >/dev/null 2>&1 || true
|
||||
rm -rf "$work_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
@@ -127,6 +147,7 @@ mount_with_udisks() {
|
||||
return 1
|
||||
fi
|
||||
local setup
|
||||
show udisksctl loop-setup -r -f "$iso"
|
||||
if ! setup="$(udisksctl loop-setup -r -f "$iso" 2>&1)"; then
|
||||
note "udisksctl loop-setup failed: $setup"
|
||||
return 1
|
||||
@@ -142,8 +163,10 @@ mount_with_udisks() {
|
||||
# A desktop session may have mounted the loop device already, which makes
|
||||
# udisksctl mount fail with AlreadyMounted; use that mount point then.
|
||||
local mounted point
|
||||
show lsblk -no MOUNTPOINT "$loop_device"
|
||||
point="$(lsblk -no MOUNTPOINT "$loop_device" 2>/dev/null | head -n 1)"
|
||||
if [[ -z "$point" ]]; then
|
||||
show udisksctl mount -b "$loop_device" -t udf -o ro
|
||||
if mounted="$(udisksctl mount -b "$loop_device" -t udf -o ro 2>&1)"; then
|
||||
# Some udisks versions end the message with a period, others do not.
|
||||
point="$(sed -n 's/^Mounted .* at \(.*\)$/\1/p' <<<"$mounted")"
|
||||
@@ -157,7 +180,7 @@ mount_with_udisks() {
|
||||
fi
|
||||
if [[ ! -d "$point" ]]; then
|
||||
note "no usable mount point for $loop_device (parsed mount point: '${point}')"
|
||||
udisksctl loop-delete -b "$loop_device" >/dev/null 2>&1 || true
|
||||
run udisksctl loop-delete -b "$loop_device" >/dev/null 2>&1 || true
|
||||
mount_method=""
|
||||
return 1
|
||||
fi
|
||||
@@ -169,7 +192,7 @@ step "Reading the ISO $iso (it is UDF, so it has to be mounted)"
|
||||
note "ISO size: $(stat -L "${size_args[@]}" "$iso") bytes"
|
||||
if [[ "$os" == Darwin ]]; then
|
||||
note "mounting with hdiutil at $mount_dir"
|
||||
hdiutil attach -readonly -nobrowse -mountpoint "$mount_dir" "$iso" >/dev/null
|
||||
run hdiutil attach -readonly -nobrowse -mountpoint "$mount_dir" "$iso" >/dev/null
|
||||
mount_method=hdiutil
|
||||
else
|
||||
note "mounting with udisksctl"
|
||||
@@ -183,20 +206,31 @@ install_wim="$mount_dir/sources/install.wim"
|
||||
[[ -d "$mount_dir/efi/boot" ]] || die "efi/boot was not found in the ISO; is it a UEFI Windows ISO?"
|
||||
note "install.wim: $(stat "${size_args[@]}" "$install_wim") bytes"
|
||||
|
||||
# The staging directory holds everything that goes into the image: the ISO files
|
||||
# without install.wim, and the install.swm parts in sources/.
|
||||
stage_dir="$work_dir/stage"
|
||||
mkdir "$stage_dir"
|
||||
step "Staging the ISO files without install.wim"
|
||||
# The UDF mount is read-only, so its directories are too; --chmod keeps the copies
|
||||
# writable, otherwise cleanup cannot remove them.
|
||||
run rsync -rt --chmod=u+rwX --exclude=/sources/install.wim "$mount_dir/" "$stage_dir/"
|
||||
if [[ "$os" == Darwin ]]; then
|
||||
# macOS's openrsync ignores --chmod, so make the copies writable here (the split
|
||||
# below writes into sources/).
|
||||
run chmod -R u+rwX "$stage_dir"
|
||||
fi
|
||||
|
||||
# Parts must stay below the 4 GiB FAT32 file limit; wimlib takes MiB.
|
||||
step "Splitting install.wim into install.swm parts (FAT32 files must stay below 4 GiB)"
|
||||
split_dir="$work_dir/split"
|
||||
mkdir "$split_dir"
|
||||
wimlib-imagex split "$install_wim" "$split_dir/install.swm" 3800 >/dev/null
|
||||
for part in "$split_dir"/install*.swm; do
|
||||
run wimlib-imagex split "$install_wim" "$stage_dir/sources/install.swm" 3800 >/dev/null
|
||||
for part in "$stage_dir"/sources/install*.swm; do
|
||||
note "$(basename "$part"): $(stat "${size_args[@]}" "$part") bytes"
|
||||
done
|
||||
|
||||
# Size the image: everything but install.wim, the split parts, and 5% plus 64 MiB
|
||||
# of room for FAT32 metadata and partition alignment.
|
||||
# Size the image: the staged files, and 5% plus 64 MiB of room for FAT32
|
||||
# metadata and partition alignment.
|
||||
content_bytes=$(
|
||||
{ find "$mount_dir" -type f ! -path "$install_wim" -exec stat "${size_args[@]}" {} +
|
||||
find "$split_dir" -type f -exec stat "${size_args[@]}" {} +; } |
|
||||
find "$stage_dir" -type f -exec stat "${size_args[@]}" {} + |
|
||||
awk '{ total += $1 } END { printf "%d", total }'
|
||||
)
|
||||
mib=$((1024 * 1024))
|
||||
@@ -204,42 +238,30 @@ image_mib=$(( (content_bytes + content_bytes / 20) / mib + 64 ))
|
||||
step "Creating a $image_mib MiB image with a GPT and one FAT32 partition"
|
||||
note "content to copy: $content_bytes bytes (without install.wim)"
|
||||
|
||||
image="$work_dir/disk.img"
|
||||
truncate -s "${image_mib}m" "$image"
|
||||
sgdisk -o -n 1:2048:0 -t 1:0700 -c 1:WINPE "$image" >/dev/null
|
||||
image="$output"
|
||||
run mkdir -p "$(dirname -- "$output")"
|
||||
# --force replaces an existing image; truncate alone would keep its old content.
|
||||
run rm -f "$image"
|
||||
partial_output="$image"
|
||||
run truncate -s "${image_mib}m" "$image"
|
||||
run sgdisk -o -n 1:2048:0 -t 1:0700 -c 1:WINPE "$image" >/dev/null
|
||||
show sgdisk -i 1 "$image"
|
||||
sectors=$(sgdisk -i 1 "$image" | awk '/^Partition size:/ { print $3 }')
|
||||
[[ -n "$sectors" ]] || die "could not read the partition size"
|
||||
mkfs.fat -F 32 -n WINPE --offset 2048 "$image" $((sectors / 2)) >/dev/null
|
||||
run mkfs.fat -F 32 -n WINPE --offset 2048 "$image" $((sectors / 2)) >/dev/null
|
||||
|
||||
# mtools addresses the partition by its byte offset inside the image.
|
||||
mtools_image="$image@@$((2048 * 512))"
|
||||
note "partition: $sectors sectors, formatted as FAT32 (label WINPE)"
|
||||
export MTOOLS_SKIP_CHECK=1
|
||||
|
||||
copy_dir() { # copy_dir SOURCE_DIR TARGET_DIR_IN_IMAGE
|
||||
local source="$1" target="$2" files=() entry
|
||||
while IFS= read -r -d '' entry; do
|
||||
files+=("$entry")
|
||||
done < <(find "$source" -maxdepth 1 -type f ! -path "$install_wim" -print0)
|
||||
if ((${#files[@]} > 0)); then
|
||||
mcopy -Q -m -i "$mtools_image" "${files[@]}" "::$target/"
|
||||
fi
|
||||
}
|
||||
|
||||
step "Copying the installation files into the image"
|
||||
copy_dir "$mount_dir" ""
|
||||
dir_count=0
|
||||
while IFS= read -r -d '' dir; do
|
||||
relative="${dir#"$mount_dir"/}"
|
||||
dir_count=$((dir_count + 1))
|
||||
mmd -i "$mtools_image" "::$relative"
|
||||
copy_dir "$dir" "$relative"
|
||||
done < <(find "$mount_dir" -mindepth 1 -type d -print0 | sort -z)
|
||||
note "copied $dir_count directories"
|
||||
note "copying the install*.swm parts to sources/"
|
||||
mcopy -Q -m -i "$mtools_image" "$split_dir"/install*.swm "::sources/"
|
||||
entries=()
|
||||
while IFS= read -r -d '' entry; do
|
||||
entries+=("$entry")
|
||||
done < <(find "$stage_dir" -mindepth 1 -maxdepth 1 -print0)
|
||||
note "\$ mcopy -s -Q -m -i $mtools_image <${#entries[@]} entries from the staging directory> ::/"
|
||||
mcopy -s -Q -m -i "$mtools_image" "${entries[@]}" "::/"
|
||||
|
||||
step "Moving the image to $output"
|
||||
mkdir -p "$(dirname -- "$output")"
|
||||
mv -f "$image" "$output"
|
||||
partial_output=""
|
||||
printf 'Built Windows 11 installation disk: %s\n' "$output"
|
||||
+150
-13
@@ -19,9 +19,12 @@ if [[ "$image_arch" == arm64 ]]; then
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
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"
|
||||
@@ -35,14 +38,18 @@ if [[ "$image_arch" == arm64 ]]; then
|
||||
# ALMA_IMAGE_SHA256 / FEDORA_IMAGE_SHA256.
|
||||
debian_sha512_default=""
|
||||
alma_sha256_default=""
|
||||
rocky_sha256_default="24692a444f1f0b8bb95375c38c8b43f8099a115347623691be2c330b40c8a1fe"
|
||||
fedora_sha256_default=""
|
||||
else
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
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"
|
||||
@@ -51,12 +58,22 @@ else
|
||||
https://cloud-images.ubuntu.com/releases/26.04/release-20260927/ubuntu-26.04-server-cloudimg-amd64.img"
|
||||
debian_sha512_default="f6978100d8031c266d55d7815ceea7fcdeacf28e1e5834fdb9c94ac96880a054a6e6f8681c2d3b0584e0057eaf3ef7353856b85212d04134744faa9b3bb1f24f"
|
||||
alma_sha256_default="47f2218668dd4776be140dd92fa3bea700be1766e2c7d88bdfd6a4b50f477b4d"
|
||||
rocky_sha256_default="92c206cc6f790c61583247eefe87890f8828420662c17cacf247cec78ab4eec8"
|
||||
fedora_sha256_default="28680fe5b371a5a82ebf43a31926e086a168e59949d03969c5093e7071f90b7f"
|
||||
ubuntu_sha256_default="8800651811af9a85465ad1d552add729947bb16488dddb4a9b5305a3d97332b2"
|
||||
fi
|
||||
|
||||
virtio_image="${tools_iso_dir}/virtio-win.iso"
|
||||
|
||||
# Win32-OpenSSH names its zip after the architecture: Win64 for x64 and ARM64
|
||||
# for arm64.
|
||||
if [[ "$image_arch" == arm64 ]]; then
|
||||
openssh_zip=OpenSSH-ARM64.zip
|
||||
else
|
||||
openssh_zip=OpenSSH-Win64.zip
|
||||
fi
|
||||
openssh_image="${tools_iso_dir}/${openssh_zip}"
|
||||
|
||||
# Windows 11 media is named after the architecture: x64 on amd64 hosts and
|
||||
# Arm64 on arm64 hosts.
|
||||
if [[ "$image_arch" == arm64 ]]; then
|
||||
@@ -98,8 +115,10 @@ windows_qcow2_path() {
|
||||
iso="$(basename -- "$iso")"
|
||||
printf '%s/%s.qcow2\n' "$base_qcow2_dir" "${iso%.*}"
|
||||
elif ! find_windows_image "$base_qcow2_dir" qcow2; then
|
||||
printf 'Error: no Windows 11 %s ISO (*Win*11*%s*.iso) found in %s\n' \
|
||||
"$windows_arch" "$windows_arch" "$install_iso_dir" >&2
|
||||
printf 'Error: no Windows 11 %s base QCOW2 image (*Win*11*%s*.qcow2) found in %s\n' \
|
||||
"$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
|
||||
fi
|
||||
}
|
||||
@@ -109,28 +128,35 @@ windows_qcow2_path() {
|
||||
# them in order.
|
||||
debian_url="${DEBIAN_IMAGE_URL:-$debian_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}"
|
||||
ubuntu_url="${UBUNTU_IMAGE_URL:-$ubuntu_url_default}"
|
||||
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}"
|
||||
debian_sha512="${DEBIAN_IMAGE_SHA512:-$debian_sha512_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}"
|
||||
ubuntu_sha256="${UBUNTU_IMAGE_SHA256:-$ubuntu_sha256_default}"
|
||||
force_download=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [--force] [all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win] ...
|
||||
Usage: $(basename "$0") [--force] [all|debian13|almalinux10|rocky9|fedora44|ubuntu2604|virtio-win|openssh-win|windows11] ...
|
||||
$(basename "$0") --print-path TARGET [--iso]
|
||||
|
||||
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.
|
||||
|
||||
When aria2c is installed and an image has several mirrors, the image is
|
||||
downloaded from all of them at once, with one connection per mirror.
|
||||
When aria2c is installed, an image is downloaded with 16 connections in total
|
||||
(DOWNLOAD_CONNECTIONS, up to 16), spread across all its mirrors if it has several.
|
||||
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 image architecture with nodejs/get-windows-iso-url.js, which needs Node.js,
|
||||
\`npm ci\` in nodejs/ and an installed Chromium (see README.md).
|
||||
|
||||
--force removes existing files and partial downloads for the selected targets,
|
||||
then downloads them again from the beginning.
|
||||
--print-path prints only the absolute image path without downloading it.
|
||||
@@ -140,6 +166,36 @@ The Windows ISO is found automatically in install-iso/ by matching
|
||||
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() {
|
||||
local target="$1"
|
||||
local media="${2:-qcow2}"
|
||||
@@ -147,14 +203,16 @@ print_image_path() {
|
||||
case "$target" in
|
||||
debian13) printf '%s\n' "$debian_image" ;;
|
||||
almalinux10) printf '%s\n' "$alma_image" ;;
|
||||
rocky9) printf '%s\n' "$rocky_image" ;;
|
||||
fedora44) printf '%s\n' "$fedora_image" ;;
|
||||
ubuntu2604) printf '%s\n' "$ubuntu_image" ;;
|
||||
virtio-win) printf '%s\n' "$virtio_image" ;;
|
||||
openssh-win) printf '%s\n' "$openssh_image" ;;
|
||||
windows11)
|
||||
if [[ "$media" == iso ]]; then
|
||||
windows_iso_path
|
||||
else
|
||||
windows_qcow2_path
|
||||
windows_base_path
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
@@ -195,7 +253,7 @@ fi
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
case "$target" in
|
||||
all|debian13|almalinux10|fedora44|ubuntu2604|virtio-win) ;;
|
||||
all|debian13|almalinux10|rocky9|fedora44|ubuntu2604|virtio-win|openssh-win|windows11) ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) printf 'Error: unknown download target: %s\n' "$target" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -258,16 +316,17 @@ download_single_source() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Download to $partial with aria2c, splitting the file across all URLs with a
|
||||
# single connection to each server.
|
||||
# Download to $partial with aria2c, splitting the file across all URLs with up
|
||||
# to DOWNLOAD_CONNECTIONS (default 16) connections in total and to each server.
|
||||
download_multi_source() {
|
||||
local partial="$1"
|
||||
shift
|
||||
local connections="${DOWNLOAD_CONNECTIONS:-16}"
|
||||
|
||||
aria2c \
|
||||
--continue=true \
|
||||
--split="$#" \
|
||||
--max-connection-per-server=1 \
|
||||
--split="$connections" \
|
||||
--max-connection-per-server="$connections" \
|
||||
--file-allocation=none \
|
||||
--auto-file-renaming=false \
|
||||
--max-tries=5 \
|
||||
@@ -286,6 +345,9 @@ download_file() {
|
||||
local destination="$3"
|
||||
local checksum_algorithm="${4:-}"
|
||||
local expected_checksum="${5:-}"
|
||||
# Signed URLs change on every request; this key identifies the source for
|
||||
# resuming a partial download instead of the URLs when it is given.
|
||||
local source_key="${6:-}"
|
||||
local partial="${destination}.part"
|
||||
# Records how the partial file was started, so that a partial download is
|
||||
# only resumed with the same downloader and URLs.
|
||||
@@ -329,7 +391,7 @@ download_file() {
|
||||
|
||||
# aria2c fills a partial file out of order, so it must not be resumed by
|
||||
# curl or wget, and vice versa.
|
||||
download_state="$(printf '%s\n' "$downloader" "${urls[@]}")"
|
||||
download_state="$(printf '%s\n' "$downloader" "${source_key:-${urls[@]}}")"
|
||||
if [[ -e "$partial" ]]; then
|
||||
if [[ ! -f "$partial_url" ]] || [[ "$(<"$partial_url")" != "$download_state" ]]; then
|
||||
printf 'Download source changed; restarting partial download.\n'
|
||||
@@ -375,6 +437,61 @@ download_file() {
|
||||
printf 'Downloaded %s:\n%s\n' "$label" "$destination"
|
||||
}
|
||||
|
||||
# The Microsoft download link is temporary, so it is fetched with headless
|
||||
# Chromium only when no matching ISO exists yet. "all" does not include it.
|
||||
download_windows_iso() {
|
||||
local iso
|
||||
local url
|
||||
local name
|
||||
|
||||
if [[ "$force_download" == true ]]; then
|
||||
printf 'Forcing a fresh download of the Windows 11 %s ISO.\n' "$windows_arch"
|
||||
while iso="$(find_windows_image "$install_iso_dir" iso)"; do
|
||||
rm -f -- "$iso"
|
||||
done
|
||||
fi
|
||||
if iso="$(find_windows_image "$install_iso_dir" iso)"; then
|
||||
printf 'Windows 11 %s ISO already exists; skipping:\n%s\n' "$windows_arch" "$iso"
|
||||
return
|
||||
fi
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
printf 'Error: node is required to get the Windows 11 ISO URL.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
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; }
|
||||
name="${url%%\?*}"
|
||||
name="${name##*/}"
|
||||
name="$(printf '%b' "${name//%/\\x}")"
|
||||
download_file \
|
||||
"Windows 11 ${windows_arch} ISO" \
|
||||
"$url" \
|
||||
"${install_iso_dir}/${name}" \
|
||||
'' \
|
||||
'' \
|
||||
"$name"
|
||||
}
|
||||
|
||||
if wants_target debian13; then
|
||||
download_file \
|
||||
'Debian 13 cloud image' \
|
||||
@@ -393,6 +510,15 @@ if wants_target almalinux10; then
|
||||
"$alma_sha256"
|
||||
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
|
||||
download_file \
|
||||
'Fedora 44 cloud image' \
|
||||
@@ -417,3 +543,14 @@ if wants_target virtio-win; then
|
||||
"$virtio_url" \
|
||||
"$virtio_image"
|
||||
fi
|
||||
|
||||
if wants_target openssh-win; then
|
||||
download_file \
|
||||
'Win32-OpenSSH zip' \
|
||||
"$openssh_url" \
|
||||
"$openssh_image"
|
||||
fi
|
||||
|
||||
if [[ " ${targets[*]} " == *" windows11 "* ]]; then
|
||||
download_windows_iso
|
||||
fi
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env node
|
||||
// Print a signed, temporary download URL for the Windows 11 ISO by driving
|
||||
// Microsoft's download page with headless Chromium.
|
||||
//
|
||||
// Usage: node get-windows-iso-url.js [--arch x64|arm64] [--lang NAME]
|
||||
// Environment:
|
||||
// PLAYWRIGHT_CORE playwright-core directory (default: node_modules here)
|
||||
// CHROME_PATH Chromium executable (default: the one Playwright installed)
|
||||
// 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.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`Usage: node get-windows-iso-url.js [options]
|
||||
|
||||
Print a signed, temporary download URL for the Windows 11 ISO (only the URL
|
||||
goes to stdout; logs go to stderr).
|
||||
|
||||
Options:
|
||||
--arch <x64|arm64> Architecture, default: the host's (x64 on amd64 hosts)
|
||||
--lang <name> Language, default "English (United States)",
|
||||
for example "Chinese Simplified"
|
||||
-h, --help Show this help
|
||||
|
||||
Environment:
|
||||
PLAYWRIGHT_CORE playwright-core directory (default: node_modules here)
|
||||
CHROME_PATH Chromium executable (default: the one Playwright installed)
|
||||
|
||||
Examples:
|
||||
node get-windows-iso-url.js --arch x64 --lang "Chinese Simplified"
|
||||
wget -c "$(node get-windows-iso-url.js 2>/dev/null)"
|
||||
|
||||
The URL expires after about 24 hours and may be bound to the IP that
|
||||
requested it.`);
|
||||
process.exit(0);
|
||||
}
|
||||
const opt = (name, def) => {
|
||||
const i = args.indexOf(name);
|
||||
return i < 0 ? def : (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true);
|
||||
};
|
||||
const hostArch = { x64: 'x64', arm64: 'arm64' }[process.arch];
|
||||
const arch = opt('--arch', hostArch);
|
||||
const lang = opt('--lang', 'English (United States)');
|
||||
const pages = { arm64: 'windows11arm64', x64: 'windows11' };
|
||||
if (!pages[arch]) {
|
||||
console.error(arch === undefined ? `unsupported host architecture ${process.arch}: pass --arch x64|arm64` : '--arch must be x64 or arm64');
|
||||
process.exit(2);
|
||||
}
|
||||
const log = (...a) => console.error('[iso]', ...a);
|
||||
|
||||
function findPlaywright() {
|
||||
if (process.env.PLAYWRIGHT_CORE) return process.env.PLAYWRIGHT_CORE;
|
||||
try { return path.dirname(require.resolve('playwright-core/package.json')); } catch {}
|
||||
console.error('playwright-core not found: run npm ci or set PLAYWRIGHT_CORE');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// CHROME_PATH wins; otherwise Playwright picks the Chromium it installed (in
|
||||
// its own per-OS cache, PLAYWRIGHT_BROWSERS_PATH if set).
|
||||
function launchOptions(chromium) {
|
||||
if (process.env.CHROME_PATH) return { executablePath: process.env.CHROME_PATH, headless: true };
|
||||
if (!fs.existsSync(chromium.executablePath())) {
|
||||
console.error('Chromium not found: set CHROME_PATH or run npx playwright-core install chromium');
|
||||
process.exit(2);
|
||||
}
|
||||
return { headless: true };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const { chromium } = require(findPlaywright());
|
||||
const browser = await chromium.launch(launchOptions(chromium));
|
||||
try {
|
||||
const ctx = await browser.newContext({
|
||||
locale: 'en-US',
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36',
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
|
||||
let api = null;
|
||||
page.on('response', async r => {
|
||||
if (/GetProductDownloadLinks/i.test(r.url())) api = await r.json().catch(() => null);
|
||||
});
|
||||
|
||||
log('opening', pages[arch]);
|
||||
await page.goto(`https://www.microsoft.com/en-us/software-download/${pages[arch]}`, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await page.click('button:has-text("Accept")', { timeout: 3000 }).catch(() => {});
|
||||
|
||||
// Pick the first real edition (currently the only multi-edition ISO).
|
||||
const edition = await page.$$eval('#product-edition option', o => o.map(x => x.value).find(v => v && v !== 'null'));
|
||||
if (!edition) throw new Error('no edition option found; the page layout may have changed');
|
||||
await page.selectOption('#product-edition', edition);
|
||||
await page.click('#submit-product-edition');
|
||||
|
||||
await page.waitForSelector('#product-languages option:not([value="null"])', { state: 'attached', timeout: 60000 });
|
||||
const langs = await page.$$eval('#product-languages option', o => o.map(x => ({ value: x.value, text: x.text })));
|
||||
const hit = langs.find(l => l.text === lang);
|
||||
if (!hit) throw new Error(`no language "${lang}"; available: ${langs.map(l => l.text).filter(t => t !== 'Choose one').join(', ')}`);
|
||||
await page.selectOption('#product-languages', hit.value);
|
||||
await page.click('#submit-sku');
|
||||
|
||||
await page.waitForSelector('a[href*="prss.microsoft.com"]', { timeout: 60000 });
|
||||
const url = await page.$eval('a[href*="prss.microsoft.com"]', a => a.href);
|
||||
const opts = api && api.ProductDownloadOptions;
|
||||
log(opts ? opts[0].Name : '(API response not captured)', '| expires:', api && api.DownloadExpirationDatetime);
|
||||
console.log(url);
|
||||
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(e => { console.error('[iso] failed:', e.message); process.exit(1); });
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "win-iso",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "win-iso",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"playwright-core": "^1.64.1"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
|
||||
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "win-iso",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "get-windows-iso-url.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"playwright-core": "^1.64.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# QEMU configuration for the Windows 11 virtual machine. The accelerators and machine type
|
||||
# are in <os>-<arch>.ini (os is linux or darwin), which QEMU reads after
|
||||
# this file.
|
||||
#
|
||||
# build-windows-base.sh fills in the file path placeholders below, writes the result
|
||||
# to <state dir>/qemu.ini and passes it to QEMU with -readconfig. Options that
|
||||
# have no -readconfig equivalent (-cpu, -display), the network and the installation
|
||||
# media are still added by the script.
|
||||
#
|
||||
# Every device here has an inbox Windows driver: the base image made from this
|
||||
# installation contains no VirtIO drivers.
|
||||
|
||||
[name]
|
||||
guest = "windows11"
|
||||
|
||||
[memory]
|
||||
size = "8G"
|
||||
|
||||
[smp-opts]
|
||||
cpus = "8"
|
||||
|
||||
# UEFI firmware: read-only code and the per-VM variable store.
|
||||
[drive "pflash0"]
|
||||
if = "pflash"
|
||||
format = "raw"
|
||||
readonly = "on"
|
||||
file = "${uefi_code}"
|
||||
|
||||
[drive "pflash1"]
|
||||
if = "pflash"
|
||||
format = "raw"
|
||||
file = "${uefi_vars}"
|
||||
|
||||
# System disk image; the device that exposes it (and the NIC, which is
|
||||
# attached to the user-mode network "net0" that the installer defines with
|
||||
# -netdev) is in the architecture file.
|
||||
[drive "system_disk"]
|
||||
if = "none"
|
||||
format = "qcow2"
|
||||
file = "${disk_image}"
|
||||
|
||||
# Register the USB tablet after vdagent so QEMU selects it as the active pointer.
|
||||
[device "usb_controller"]
|
||||
driver = "qemu-xhci"
|
||||
|
||||
[device "tablet0"]
|
||||
driver = "usb-tablet"
|
||||
bus = "usb_controller.0"
|
||||
|
||||
# The QEMU monitor, on a socket in the state directory: build-windows-base.sh
|
||||
# presses a key through it at the "Press any key to boot from CD or DVD" prompt.
|
||||
[chardev "monitor"]
|
||||
backend = "socket"
|
||||
path = "${monitor_socket}"
|
||||
server = "on"
|
||||
wait = "off"
|
||||
|
||||
[mon "monitor0"]
|
||||
chardev = "monitor"
|
||||
mode = "readline"
|
||||
@@ -0,0 +1,33 @@
|
||||
# macOS host, arm64 part of the Windows 11 QEMU configuration; read after config.ini.
|
||||
|
||||
# Accelerators are tried in order: HVF on macOS, else emulation.
|
||||
[accel]
|
||||
accel = "hvf"
|
||||
|
||||
[accel]
|
||||
accel = "tcg"
|
||||
|
||||
# build-windows-base.sh adds kernel-irqchip=off unless installing with
|
||||
# --from-img (see there).
|
||||
[machine]
|
||||
type = "virt"
|
||||
|
||||
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
||||
# installation media) both have inbox Windows drivers.
|
||||
[device "system_disk-dev"]
|
||||
driver = "nvme"
|
||||
drive = "system_disk"
|
||||
serial = "win11disk"
|
||||
bootindex = "1"
|
||||
|
||||
# Windows has no inbox driver for virtio-net or virtio-gpu, so the network card
|
||||
# is an Intel e1000e and the display is ramfb (a fixed-size framebuffer).
|
||||
[device "nic0"]
|
||||
driver = "e1000e"
|
||||
netdev = "net0"
|
||||
|
||||
[device "gpu0"]
|
||||
driver = "ramfb"
|
||||
|
||||
[device "kbd0"]
|
||||
driver = "usb-kbd"
|
||||
@@ -0,0 +1,23 @@
|
||||
# Linux host, amd64 part of the Windows 11 QEMU configuration; read after config.ini.
|
||||
|
||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||
[accel]
|
||||
accel = "kvm"
|
||||
|
||||
[accel]
|
||||
accel = "tcg"
|
||||
|
||||
[machine]
|
||||
type = "q35"
|
||||
|
||||
# The system disk is attached through IDE so that Windows can boot before the
|
||||
# VirtIO storage driver is installed; the installation media use ide.1 to ide.3.
|
||||
[device "system_disk-dev"]
|
||||
driver = "ide-hd"
|
||||
drive = "system_disk"
|
||||
bus = "ide.0"
|
||||
bootindex = "1"
|
||||
|
||||
[device "nic0"]
|
||||
driver = "e1000e"
|
||||
netdev = "net0"
|
||||
@@ -0,0 +1,31 @@
|
||||
# Linux host, arm64 part of the Windows 11 QEMU configuration; read after config.ini.
|
||||
|
||||
# Accelerators are tried in order: KVM on Linux, else emulation.
|
||||
[accel]
|
||||
accel = "kvm"
|
||||
|
||||
[accel]
|
||||
accel = "tcg"
|
||||
|
||||
[machine]
|
||||
type = "virt"
|
||||
|
||||
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
|
||||
# installation media) both have inbox Windows drivers.
|
||||
[device "system_disk-dev"]
|
||||
driver = "nvme"
|
||||
drive = "system_disk"
|
||||
serial = "win11disk"
|
||||
bootindex = "1"
|
||||
|
||||
# Windows has no inbox driver for virtio-net or virtio-gpu, so the network card
|
||||
# is an Intel e1000e and the display is ramfb (a fixed-size framebuffer).
|
||||
[device "nic0"]
|
||||
driver = "e1000e"
|
||||
netdev = "net0"
|
||||
|
||||
[device "gpu0"]
|
||||
driver = "ramfb"
|
||||
|
||||
[device "kbd0"]
|
||||
driver = "usb-kbd"
|
||||
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
<settings pass="windowsPE">
|
||||
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<SetupUILanguage>
|
||||
<UILanguage>en-US</UILanguage>
|
||||
</SetupUILanguage>
|
||||
<InputLocale>en-US</InputLocale>
|
||||
<SystemLocale>en-US</SystemLocale>
|
||||
<UILanguage>en-US</UILanguage>
|
||||
<UserLocale>en-US</UserLocale>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<DynamicUpdate>
|
||||
<Enable>false</Enable>
|
||||
<WillShowUI>Never</WillShowUI>
|
||||
</DynamicUpdate>
|
||||
<DiskConfiguration>
|
||||
<Disk wcm:action="add">
|
||||
<DiskID>0</DiskID>
|
||||
<WillWipeDisk>true</WillWipeDisk>
|
||||
<CreatePartitions>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Type>EFI</Type>
|
||||
<Size>100</Size>
|
||||
</CreatePartition>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Type>MSR</Type>
|
||||
<Size>16</Size>
|
||||
</CreatePartition>
|
||||
<CreatePartition wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Type>Primary</Type>
|
||||
<Extend>true</Extend>
|
||||
</CreatePartition>
|
||||
</CreatePartitions>
|
||||
<ModifyPartitions>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<PartitionID>1</PartitionID>
|
||||
<Format>FAT32</Format>
|
||||
<Label>System</Label>
|
||||
</ModifyPartition>
|
||||
<ModifyPartition wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<PartitionID>3</PartitionID>
|
||||
<Format>NTFS</Format>
|
||||
<Label>Windows</Label>
|
||||
<Letter>C</Letter>
|
||||
</ModifyPartition>
|
||||
</ModifyPartitions>
|
||||
</Disk>
|
||||
<WillShowUI>OnError</WillShowUI>
|
||||
</DiskConfiguration>
|
||||
<ImageInstall>
|
||||
<OSImage>
|
||||
<InstallFrom>
|
||||
<MetaData wcm:action="add">
|
||||
<Key>/IMAGE/INDEX</Key>
|
||||
<Value>6</Value>
|
||||
</MetaData>
|
||||
</InstallFrom>
|
||||
<InstallTo>
|
||||
<DiskID>0</DiskID>
|
||||
<PartitionID>3</PartitionID>
|
||||
</InstallTo>
|
||||
<WillShowUI>OnError</WillShowUI>
|
||||
</OSImage>
|
||||
</ImageInstall>
|
||||
<UserData>
|
||||
<AcceptEula>true</AcceptEula>
|
||||
<ProductKey>
|
||||
<Key>W269N-WFGWX-YVC9B-4J6C9-T83GX</Key>
|
||||
<WillShowUI>Never</WillShowUI>
|
||||
</ProductKey>
|
||||
</UserData>
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>reg add HKLM\SYSTEM\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
<!-- After Setup, boot into audit mode instead of OOBE: Windows logs on as the
|
||||
built-in Administrator (no password, no account is created) and runs the
|
||||
auditUser commands below, the last of which generalizes the image and powers
|
||||
off. So the answer file holds no credentials. -->
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<Reseal>
|
||||
<Mode>Audit</Mode>
|
||||
</Reseal>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="auditUser">
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Description>Disable the built-in Administrator again</Description>
|
||||
<Path>net.exe user Administrator /active:no</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Description>Turn hibernation off, which deletes hiberfil.sys (3 GB) from the image</Description>
|
||||
<Path>powercfg.exe /hibernate off</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Description>Remove the cached copies of this answer file (Windows would read
|
||||
them before the Unattend.xml on a seed ISO) and the setup logs</Description>
|
||||
<Path>cmd.exe /c "del /f /q C:\Windows\Panther\unattend*.xml C:\Windows\Panther\Unattend\unattend*.xml C:\Windows\Panther\*.log & rd /s /q C:\Windows\Panther\UnattendGC & exit /b 0"</Path>
|
||||
</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 "& ('{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>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
@@ -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."
|
||||
Reference in new issue
Block a user