Support arm64 hosts in the Windows 11 launcher

Pick qemu-system-aarch64, the AArch64 UEFI firmware and a per-architecture
QEMU config (NVMe disk, virtio-net, ramfb, USB installation media) on arm64,
render the answer file for the target architecture, and move the q35-only
disk and NIC devices out of the shared config. Update vm-images to match
arm64 Windows ISO names.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
guofangandClaude Sonnet 5.5 committed 2026-10-17 14:23:44 +08:00
1 parent 5feed51c61
commit c13ebc7d11
6 files changed
+124 -49

No files matched your search

+13
View File
@@ -2,3 +2,16 @@
[machine] [machine]
type = "q35" type = "q35"
# The system disk is attached through IDE so that Windows can boot before the
# VirtIO storage driver is installed; the installation media of
# --install-from-iso 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"
+27
View File
@@ -0,0 +1,27 @@
# arm64 part of the Windows 11 QEMU configuration; read after windows11.ini.
[machine]
type = "virt"
# The "virt" machine has no IDE. NVMe and USB mass storage (used by the
# installation media of --install-from-iso) 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; the guest tools installed by
# Configure-Windows.ps1 provide it.
[device "nic0"]
driver = "virtio-net-pci"
netdev = "net0"
# Unlike q35, "virt" has no default GPU or keyboard. ramfb is used rather than
# virtio-gpu because the firmware's virtio-gpu output freezes at the Windows
# boot manager; Windows drives either one through its basic display driver.
[device "gpu0"]
driver = "ramfb"
[device "kbd0"]
driver = "usb-kbd"
+5 -17
View File
@@ -1,5 +1,5 @@
# QEMU configuration for the Windows 11 virtual machine. The machine type is in # QEMU configuration for the Windows 11 virtual machine. The machine type is in
# windows11-amd64.ini, which QEMU reads after this file. # windows11-<arch>.ini, which QEMU reads after this file.
# #
# run-windows11.py fills in the file path placeholders below, writes the result # run-windows11.py fills in the file path placeholders below, writes the result
# to <state dir>/qemu.ini and passes it to QEMU with -readconfig. Options that # to <state dir>/qemu.ini and passes it to QEMU with -readconfig. Options that
@@ -37,27 +37,15 @@
format = "raw" format = "raw"
file = "${uefi_vars}" file = "${uefi_vars}"
# System disk. It is attached through IDE so that Windows can boot before the # System disk image; the device that exposes it (and the NIC, which is
# VirtIO storage driver is installed; the installation media of # attached to the user-mode network "net0" that the launcher defines with
# --install-from-iso use ide.1 to ide.3. # -netdev because a config file section cannot hold more than one hostfwd) is in
# the architecture file.
[drive "system_disk"] [drive "system_disk"]
if = "none" if = "none"
format = "qcow2" format = "qcow2"
file = "${disk_image}" file = "${disk_image}"
[device "system_disk-dev"]
driver = "ide-hd"
drive = "system_disk"
bus = "ide.0"
bootindex = "1"
# The NIC is attached to the user-mode network "net0", which the launcher
# defines with -netdev because a config file section cannot hold more than one
# hostfwd (Remote Desktop and SSH are both forwarded to the host).
[device "nic0"]
driver = "e1000e"
netdev = "net0"
# Host/guest clipboard integration through the SPICE vdagent channel. # Host/guest clipboard integration through the SPICE vdagent channel.
[device "virtio_serial"] [device "virtio_serial"]
driver = "virtio-serial-pci" driver = "virtio-serial-pci"
+63 -30
View File
@@ -67,7 +67,19 @@ rdp_port = env("WIN11_VM_RDP_PORT") or "3389"
ssh_port = env("WIN11_VM_SSH_PORT") or "2222" ssh_port = env("WIN11_VM_SSH_PORT") or "2222"
qemu_config_template = Path( qemu_config_template = Path(
env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11.ini") env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11.ini")
qemu_arch_config = script_dir / "qemu" / "windows11-amd64.ini"
def normalize_arch(arch):
if arch in ("arm64", "aarch64"):
return "arm64"
if arch in ("x86_64", "amd64"):
return "amd64"
die(f"unsupported architecture: {arch}")
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or platform.machine())
qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
qemu_arch_config = script_dir / "qemu" / f"windows11-{host_arch}.ini"
def pick_firmware(override, *candidates): def pick_firmware(override, *candidates):
@@ -79,20 +91,36 @@ def pick_firmware(override, *candidates):
return Path(candidates[0]) return Path(candidates[0])
uefi_code = pick_firmware( if host_arch == "arm64":
env("WIN11_VM_UEFI_CODE"), uefi_code = pick_firmware(
"/usr/share/OVMF/OVMF_CODE_4M.fd", env("WIN11_VM_UEFI_CODE"),
"/usr/share/edk2/ovmf/OVMF_CODE.fd", "/usr/share/AAVMF/AAVMF_CODE.fd",
"/opt/homebrew/share/qemu/edk2-x86_64-code.fd", "/usr/share/edk2/aarch64/QEMU_EFI.fd",
"/usr/local/share/qemu/edk2-x86_64-code.fd", "/opt/homebrew/share/qemu/edk2-aarch64-code.fd",
) "/usr/local/share/qemu/edk2-aarch64-code.fd",
uefi_vars_template = pick_firmware( )
env("WIN11_VM_UEFI_VARS"), uefi_vars_template = pick_firmware(
"/usr/share/OVMF/OVMF_VARS_4M.fd", env("WIN11_VM_UEFI_VARS"),
"/usr/share/edk2/ovmf/OVMF_VARS.fd", "/usr/share/AAVMF/AAVMF_VARS.fd",
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", "/usr/share/edk2/aarch64/vars-template-pflash.raw",
"/usr/local/share/qemu/edk2-i386-vars.fd", "/opt/homebrew/share/qemu/edk2-arm-vars.fd",
) "/usr/local/share/qemu/edk2-arm-vars.fd",
)
else:
uefi_code = pick_firmware(
env("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(
env("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",
)
daemonize = False daemonize = False
install_from_iso = False install_from_iso = False
download_only = False download_only = False
@@ -108,8 +136,8 @@ If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from
the Windows 11 QCOW2 base image. the Windows 11 QCOW2 base image.
The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in The VM hardware (accelerator, memory, CPUs, drives, devices) is defined in
qemu/windows11.ini, which is rendered to qemu.ini in the state directory and qemu/windows11.ini, which is rendered to qemu.ini in the state directory and
loaded with QEMU's -readconfig, followed by qemu/windows11-amd64.ini. Later QEMU loaded with QEMU's -readconfig, followed by qemu/windows11-<arch>.ini (amd64 or arm64, chosen from the host
arguments override them, e.g. -- -m 4G -smp 4. architecture or VM_IMAGE_ARCH). Later QEMU arguments override them, e.g. -- -m 4G -smp 4.
Use --install-from-iso to perform an automatic ISO installation instead. Use --install-from-iso to perform an automatic ISO installation instead.
If it exists, start Windows normally without attaching installation media. If it exists, start Windows normally without attaching installation media.
@@ -226,8 +254,8 @@ if not disk_image.is_file():
base_image = Path(env("WIN11_VM_BASE_IMAGE") base_image = Path(env("WIN11_VM_BASE_IMAGE")
or capture([str(image_downloader), "--print-path", "windows11"])) or capture([str(image_downloader), "--print-path", "windows11"]))
if shutil.which("qemu-system-x86_64") is None: if shutil.which(qemu_binary) is None:
die("qemu-system-x86_64 is not installed.") die(f"{qemu_binary} is not installed.")
if installing: if installing:
if shutil.which("qemu-img") is None: if shutil.which("qemu-img") is None:
die("qemu-img is not installed.") die("qemu-img is not installed.")
@@ -259,7 +287,7 @@ if installing_from_iso:
return source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime return source.exists() and source.stat().st_mtime > seed_iso.stat().st_mtime
if not seed_iso.is_file() or any(newer_than_seed(s) for s in seed_sources): if not seed_iso.is_file() or any(newer_than_seed(s) for s in seed_sources):
run([str(script_dir / "seediso" / "windows11" / "build-seed.sh"), str(seed_iso)]) run([str(script_dir / "seediso" / "windows11" / "build-seed.sh"), str(seed_iso), host_arch])
if pid_file.is_file(): if pid_file.is_file():
old_pid = pid_file.read_text().strip() old_pid = pid_file.read_text().strip()
@@ -336,14 +364,19 @@ qemu_args = [
# "-display", "none", "-spice", "port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off", "-device", "virtio-vga", # "-display", "none", "-spice", "port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off", "-device", "virtio-vga",
] ]
if installing_from_iso: if installing_from_iso:
qemu_args += [ # q35 has IDE for the installation media; "virt" does not, so use USB.
"-drive", f"file={install_iso},if=none,id=install_media,media=cdrom,readonly=on", for media_id, media_file, boot_index, ide_bus in (
"-device", "ide-cd,drive=install_media,bus=ide.1,bootindex=2", ("install_media", install_iso, 2, "ide.1"),
"-drive", f"file={seed_iso},if=none,id=answer_media,media=cdrom,readonly=on", ("answer_media", seed_iso, 3, "ide.2"),
"-device", "ide-cd,drive=answer_media,bus=ide.2,bootindex=3", ("tools_media", tools_iso, 4, "ide.3")):
"-drive", f"file={tools_iso},if=none,id=tools_media,media=cdrom,readonly=on", device = (f"ide-cd,drive={media_id},bus={ide_bus},bootindex={boot_index}"
"-device", "ide-cd,drive=tools_media,bus=ide.3,bootindex=4", if host_arch == "amd64"
] else f"usb-storage,drive={media_id},bootindex={boot_index}")
qemu_args += [
"-drive", f"file={media_file},if=none,id={media_id},media=cdrom,readonly=on",
"-device", device,
]
if installing_from_iso: if installing_from_iso:
print("Starting Windows 11 automatic installation.") print("Starting Windows 11 automatic installation.")
elif installing: elif installing:
@@ -357,7 +390,7 @@ print("Account: guofang / password; " + "; ".join(
if daemonize: if daemonize:
log_file.write_text("") log_file.write_text("")
qemu_args += ["-serial", f"file:{log_file}", "-daemonize", "-pidfile", str(pid_file)] qemu_args += ["-serial", f"file:{log_file}", "-daemonize", "-pidfile", str(pid_file)]
run(["qemu-system-x86_64", *qemu_args, *extra_args]) run([qemu_binary, *qemu_args, *extra_args])
print(f"VM started (PID {pid_file.read_text().strip()}); console log: {log_file}") print(f"VM started (PID {pid_file.read_text().strip()}); console log: {log_file}")
else: else:
os.execvp("qemu-system-x86_64", ["qemu-system-x86_64", *qemu_args, *extra_args]) os.execvp(qemu_binary, [qemu_binary, *qemu_args, *extra_args])
+15 -1
View File
@@ -3,8 +3,22 @@ set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
output="${1:-${script_dir}/seed.iso}" output="${1:-${script_dir}/seed.iso}"
arch="${2:-${VM_IMAGE_ARCH:-$(uname -m)}}"
case "$arch" in
arm64|aarch64) answer_arch=arm64 ;;
x86_64|amd64) answer_arch=amd64 ;;
*) printf 'Error: unsupported architecture: %s\n' "$arch" >&2; exit 1 ;;
esac
# The answer file's components name the processor architecture, so render a
# copy for the target architecture.
work_dir="$(mktemp -d)"
trap 'rm -rf "$work_dir"' EXIT
sed "s/processorArchitecture=\"amd64\"/processorArchitecture=\"${answer_arch}\"/" \
"$script_dir/Autounattend.xml" >"$work_dir/Autounattend.xml"
seed_files=( seed_files=(
"$script_dir/Autounattend.xml" "$work_dir/Autounattend.xml"
"$script_dir/Configure-Windows.ps1" "$script_dir/Configure-Windows.ps1"
) )
# Numbered installer scripts (10-Install-Git.bat, 20-..., in install order). # Numbered installer scripts (10-Install-Git.bat, 20-..., in install order).