diff --git a/qemu/windows11-amd64.ini b/qemu/windows11-amd64.ini index 688f818..ce18c9a 100644 --- a/qemu/windows11-amd64.ini +++ b/qemu/windows11-amd64.ini @@ -2,3 +2,16 @@ [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 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" diff --git a/qemu/windows11-arm64.ini b/qemu/windows11-arm64.ini new file mode 100644 index 0000000..7f1e35f --- /dev/null +++ b/qemu/windows11-arm64.ini @@ -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" diff --git a/qemu/windows11.ini b/qemu/windows11.ini index 49f236a..c11da63 100644 --- a/qemu/windows11.ini +++ b/qemu/windows11.ini @@ -1,5 +1,5 @@ # QEMU configuration for the Windows 11 virtual machine. The machine type is in -# windows11-amd64.ini, which QEMU reads after this file. +# windows11-.ini, which QEMU reads after this file. # # run-windows11.py fills in the file path placeholders below, writes the result # to /qemu.ini and passes it to QEMU with -readconfig. Options that @@ -37,27 +37,15 @@ format = "raw" file = "${uefi_vars}" -# System disk. It 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. +# System disk image; the device that exposes it (and the NIC, which is +# attached to the user-mode network "net0" that the launcher defines with +# -netdev because a config file section cannot hold more than one hostfwd) is in +# the architecture file. [drive "system_disk"] if = "none" format = "qcow2" 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. [device "virtio_serial"] driver = "virtio-serial-pci" diff --git a/run-windows11.py b/run-windows11.py index 9b06ba2..0d6d7a3 100755 --- a/run-windows11.py +++ b/run-windows11.py @@ -67,7 +67,19 @@ rdp_port = env("WIN11_VM_RDP_PORT") or "3389" ssh_port = env("WIN11_VM_SSH_PORT") or "2222" qemu_config_template = Path( 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): @@ -79,20 +91,36 @@ def pick_firmware(override, *candidates): return Path(candidates[0]) -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", -) +if host_arch == "arm64": + uefi_code = pick_firmware( + env("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( + env("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( + 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 install_from_iso = 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 VM hardware (accelerator, memory, CPUs, drives, devices) is defined in 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 -arguments override them, e.g. -- -m 4G -smp 4. +loaded with QEMU's -readconfig, followed by qemu/windows11-.ini (amd64 or arm64, chosen from the host +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. 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") or capture([str(image_downloader), "--print-path", "windows11"])) -if shutil.which("qemu-system-x86_64") is None: - die("qemu-system-x86_64 is not installed.") +if shutil.which(qemu_binary) is None: + die(f"{qemu_binary} is not installed.") if installing: if shutil.which("qemu-img") is None: 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 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(): 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", ] if installing_from_iso: - qemu_args += [ - "-drive", f"file={install_iso},if=none,id=install_media,media=cdrom,readonly=on", - "-device", "ide-cd,drive=install_media,bus=ide.1,bootindex=2", - "-drive", f"file={seed_iso},if=none,id=answer_media,media=cdrom,readonly=on", - "-device", "ide-cd,drive=answer_media,bus=ide.2,bootindex=3", - "-drive", f"file={tools_iso},if=none,id=tools_media,media=cdrom,readonly=on", - "-device", "ide-cd,drive=tools_media,bus=ide.3,bootindex=4", - ] + # q35 has IDE for the installation media; "virt" does not, so use USB. + for media_id, media_file, boot_index, ide_bus in ( + ("install_media", install_iso, 2, "ide.1"), + ("answer_media", seed_iso, 3, "ide.2"), + ("tools_media", tools_iso, 4, "ide.3")): + device = (f"ide-cd,drive={media_id},bus={ide_bus},bootindex={boot_index}" + 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: print("Starting Windows 11 automatic installation.") elif installing: @@ -357,7 +390,7 @@ print("Account: guofang / password; " + "; ".join( if daemonize: log_file.write_text("") 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}") 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]) diff --git a/seediso/windows11/build-seed.sh b/seediso/windows11/build-seed.sh index 0f932ad..4f5f4ad 100755 --- a/seediso/windows11/build-seed.sh +++ b/seediso/windows11/build-seed.sh @@ -3,8 +3,22 @@ set -euo pipefail script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" 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=( - "$script_dir/Autounattend.xml" + "$work_dir/Autounattend.xml" "$script_dir/Configure-Windows.ps1" ) # Numbered installer scripts (10-Install-Git.bat, 20-..., in install order). diff --git a/vm-images b/vm-images index 6286495..4d9e1b4 160000 --- a/vm-images +++ b/vm-images @@ -1 +1 @@ -Subproject commit 6286495d27836300a2cb5d621728108b7858c33a +Subproject commit 4d9e1b43d932d4788cb523999f0b538307d74a11