Load the Windows 11 VM hardware from QEMU config files and add port options

Move the QEMU hardware definition of the Windows 11 VM into
qemu/windows11.ini and qemu/windows11-amd64.ini, loaded with -readconfig
like the Linux launchers. The network stays in the launcher because a
config file section cannot hold two hostfwd entries. Add --rdp-port and
--ssh-port, which override the WIN11_VM_*_PORT environment variables.
WIN11_VM_MEMORY and WIN11_VM_CPUS are replaced by the config file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GuofangandClaude Sonnet 5.5 committed 2026-10-15 12:36:15 +08:00
1 parent 26baf25172
commit e1e5a2149c
4 files changed
+179 -29

No files matched your search

+7 -1
View File
@@ -175,6 +175,12 @@ passwordless administrator login. The installer includes the VirtIO drivers, SPI
and QEMU Guest Agent. Connect with `ssh guofang@localhost -p 2222`. The launcher and QEMU Guest Agent. Connect with `ssh guofang@localhost -p 2222`. The launcher
exposes the vdagent channel for host/guest clipboard integration and the QEMU Guest Agent at exposes the vdagent channel for host/guest clipboard integration and the QEMU Guest Agent at
`.vm/windows11/qemu-guest-agent.sock`. Change the password after first login. `.vm/windows11/qemu-guest-agent.sock`. Change the password after first login.
The Windows VM hardware (accelerator, 8G memory, 8 CPUs, system disk, devices)
is defined in `qemu/windows11.ini`, with the machine type in
`qemu/windows11-amd64.ini`; the launcher renders the former to
`.vm/windows11/qemu.ini` and loads both with `-readconfig`. Only the network
and the installation media are still set by the launcher. QEMU arguments after
`--` override the config files, for example `./run-windows11.py -- -m 4G -smp 4`.
The generated answer-file ISO is stored beside the disk as The generated answer-file ISO is stored beside the disk as
`.vm/windows11/seed.iso`. `.vm/windows11/seed.iso`.
@@ -183,7 +189,7 @@ To install from the Windows ISO instead, use `./run-windows11.py
disk; subsequent starts always boot the existing system disk normally. During disk; subsequent starts always boot the existing system disk normally. During
ISO installation, the launcher resolves its tools ISO through the shared image ISO installation, the launcher resolves its tools ISO through the shared image
helper and downloads it when missing. Override the base QCOW2, source ISO, helper and downloads it when missing. Override the base QCOW2, source ISO,
tools ISO, resources, RDP port, or SSH port with the `WIN11_VM_` environment tools ISO, RDP port, or SSH port with the `WIN11_VM_` environment
variables shown by `./run-windows11.py --help`. For example, when the VirtIO ISO variables shown by `./run-windows11.py --help`. For example, when the VirtIO ISO
is elsewhere: is elsewhere:
+4
View File
@@ -0,0 +1,4 @@
# amd64 part of the Windows 11 QEMU configuration; read after windows11.ini.
[machine]
type = "q35"
+104
View File
@@ -0,0 +1,104 @@
# QEMU configuration for the Windows 11 virtual machine. The machine type is in
# windows11-amd64.ini, which QEMU reads after this file.
#
# 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
# have no -readconfig equivalent (-cpu, -display) and the installation media
# attached by --install-from-iso are still added by the launcher.
[name]
guest = "windows11"
# Accelerators are tried in order: KVM on Linux, HVF on macOS, else emulation.
[accel]
accel = "kvm"
[accel]
accel = "hvf"
[accel]
accel = "tcg"
[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. 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.
[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"
[chardev "vdagent"]
backend = "qemu-vdagent"
name = "vdagent"
clipboard = "on"
mouse = "off"
[device "vdagent-port"]
driver = "virtserialport"
chardev = "vdagent"
name = "com.redhat.spice.0"
# 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"
# QEMU Guest Agent and the QEMU monitor, both on sockets in the state directory.
[chardev "qga"]
backend = "socket"
path = "${guest_agent_socket}"
server = "on"
wait = "off"
[device "qga-port"]
driver = "virtserialport"
chardev = "qga"
name = "org.qemu.guest_agent.0"
[chardev "monitor"]
backend = "socket"
path = "${monitor_socket}"
server = "on"
wait = "off"
[mon "monitor0"]
chardev = "monitor"
mode = "readline"
+64 -28
View File
@@ -4,6 +4,7 @@
import os import os
import platform import platform
import shutil import shutil
import string
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -50,10 +51,11 @@ def find_image_downloader():
state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / ".vm" / "windows11") state_dir = Path(env("WIN11_VM_STATE_DIR") or script_dir / ".vm" / "windows11")
disk_size = env("WIN11_VM_DISK_SIZE") or "64G" disk_size = env("WIN11_VM_DISK_SIZE") or "64G"
memory = env("WIN11_VM_MEMORY") or "8G"
cpus = env("WIN11_VM_CPUS") or "8"
rdp_port = env("WIN11_VM_RDP_PORT") or "3389" 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(
env("WIN11_VM_QEMU_CONFIG") or script_dir / "qemu" / "windows11.ini")
qemu_arch_config = script_dir / "qemu" / "windows11-amd64.ini"
def pick_firmware(override, *candidates): def pick_firmware(override, *candidates):
@@ -87,10 +89,15 @@ extra_args = []
def usage(stream=sys.stdout): def usage(stream=sys.stdout):
stream.write(f"""\ stream.write(f"""\
Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--install-from-iso] [--state-dir DIR] [-- QEMU_ARGUMENT ...] Usage: {Path(sys.argv[0]).name} [--daemon] [--download-only] [--install-from-iso] [--state-dir DIR]
[--rdp-port PORT] [--ssh-port PORT] [-- QEMU_ARGUMENT ...]
If the system disk (DIR/disk.qcow2, see --state-dir) is missing, create it from 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
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.
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.
@@ -101,14 +108,16 @@ Options:
--install-from-iso Install from ISO when the system disk is missing --install-from-iso Install from ISO when the system disk is missing
--state-dir DIR Directory for the VM disk, seed ISO and runtime files --state-dir DIR Directory for the VM disk, seed ISO and runtime files
(default: .vm/windows11; overrides WIN11_VM_STATE_DIR) (default: .vm/windows11; overrides WIN11_VM_STATE_DIR)
--rdp-port PORT Host port forwarded to guest RDP (default: 3389;
overrides WIN11_VM_RDP_PORT)
--ssh-port PORT Host port forwarded to guest SSH (default: 2222;
overrides WIN11_VM_SSH_PORT)
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images) WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images)
WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso
WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper) WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper)
WIN11_VM_MEMORY Guest memory (default: 8G)
WIN11_VM_CPUS Virtual CPU count (default: 8)
WIN11_VM_RDP_PORT Host RDP port (default: 3389) WIN11_VM_RDP_PORT Host RDP port (default: 3389)
WIN11_VM_SSH_PORT Host SSH port (default: 2222) WIN11_VM_SSH_PORT Host SSH port (default: 2222)
WIN11_VM_STATE_DIR Runtime directory (default: .vm/windows11) WIN11_VM_STATE_DIR Runtime directory (default: .vm/windows11)
@@ -117,6 +126,8 @@ Environment variables:
WIN11_VM_SEED_ISO Answer-file ISO path (default: alongside the VM disk) WIN11_VM_SEED_ISO Answer-file ISO path (default: alongside the VM disk)
WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset) WIN11_VM_UEFI_CODE OVMF read-only firmware path (auto-detected if unset)
WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset) WIN11_VM_UEFI_VARS OVMF variable-store template path (auto-detected if unset)
WIN11_VM_QEMU_CONFIG QEMU config template passed to -readconfig
(default: qemu/windows11.ini)
WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere) WIN11_VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
""") """)
@@ -136,6 +147,18 @@ while args:
state_dir = Path(args.pop(0)) state_dir = Path(args.pop(0))
elif arg.startswith("--state-dir="): elif arg.startswith("--state-dir="):
state_dir = Path(arg.split("=", 1)[1]) state_dir = Path(arg.split("=", 1)[1])
elif arg in ("--rdp-port", "--ssh-port") or arg.startswith(("--rdp-port=", "--ssh-port=")):
name, sep, value = arg.partition("=")
if not sep:
if not args:
die(f"{name} requires a port argument.", 2)
value = args.pop(0)
if not (value.isascii() and value.isdigit() and 1 <= int(value) <= 65535):
die(f"{name} must be a port number from 1 to 65535: {value}", 2)
if name == "--rdp-port":
rdp_port = value
else:
ssh_port = value
elif arg in ("-h", "--help"): elif arg in ("-h", "--help"):
usage() usage()
sys.exit(0) sys.exit(0)
@@ -170,6 +193,7 @@ pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log" log_file = state_dir / "console.log"
monitor_socket = state_dir / "qemu-monitor.sock" monitor_socket = state_dir / "qemu-monitor.sock"
guest_agent_socket = state_dir / "qemu-guest-agent.sock" guest_agent_socket = state_dir / "qemu-guest-agent.sock"
qemu_config = state_dir / "qemu.ini"
installing = False installing = False
installing_from_iso = False installing_from_iso = False
@@ -249,23 +273,41 @@ if installing:
display_backend = env("WIN11_VM_DISPLAY") or ( display_backend = env("WIN11_VM_DISPLAY") or (
"cocoa" if platform.system() == "Darwin" else "gtk") "cocoa" if platform.system() == "Darwin" else "gtk")
# Most of the VM definition lives in the QEMU config template; fill in the
# file paths and hand the rendered file to QEMU with -readconfig.
config_values = {
"uefi_code": uefi_code,
"uefi_vars": uefi_vars,
"disk_image": disk_image,
"guest_agent_socket": guest_agent_socket,
"monitor_socket": monitor_socket,
}
for key, value in config_values.items():
# QEMU's config file parser has no escape syntax for quoted values.
if any(char in str(value) for char in '"\n'):
die(f"{key} must not contain double quotes or newlines: {value}")
for config_file in (qemu_config_template, qemu_arch_config):
if not config_file.is_file():
die(f"QEMU config file was not found: {config_file}")
try:
qemu_config_text = string.Template(qemu_config_template.read_text()).substitute(
{key: str(value) for key, value in config_values.items()})
except (KeyError, ValueError) as error:
die(f"invalid placeholder in {qemu_config_template}: {error}")
qemu_config.write_text(qemu_config_text)
qemu_args = [ qemu_args = [
"-name", "windows11", "-machine", "q35", "-m", memory, "-smp", cpus, "-readconfig", str(qemu_config),
"-readconfig", str(qemu_arch_config),
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
"-cpu", "max",
# A config file section cannot hold two hostfwd entries, so the network
# that the NIC in the config file uses is defined here. -nic none stops
# QEMU from adding its default NIC as well.
"-netdev", f"user,id=net0,hostfwd=tcp::{rdp_port}-:3389,hostfwd=tcp::{ssh_port}-:22",
"-nic", "none",
"-display", display_backend, "-display", display_backend,
# "-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",
"-drive", f"if=pflash,format=raw,readonly=on,file={uefi_code}",
"-drive", f"if=pflash,format=raw,file={uefi_vars}",
"-drive", f"file={disk_image},if=none,id=system_disk,format=qcow2",
"-device", "ide-hd,drive=system_disk,bus=ide.0,bootindex=1",
"-nic", f"user,model=e1000e,hostfwd=tcp::{rdp_port}-:3389,hostfwd=tcp::{ssh_port}-:22",
"-device", "virtio-serial-pci,id=virtio_serial",
"-chardev", "qemu-vdagent,id=vdagent,name=vdagent,clipboard=on,mouse=off",
"-device", "virtserialport,chardev=vdagent,name=com.redhat.spice.0",
# Register the USB tablet after vdagent so QEMU selects it as the active pointer.
"-device", "qemu-xhci,id=usb_controller", "-device", "usb-tablet,bus=usb_controller.0",
"-chardev", f"socket,path={guest_agent_socket},server=on,wait=off,id=qga",
"-device", "virtserialport,chardev=qga,name=org.qemu.guest_agent.0",
"-monitor", f"unix:{monitor_socket},server=on,wait=off",
] ]
if installing_from_iso: if installing_from_iso:
qemu_args += [ qemu_args += [
@@ -276,18 +318,12 @@ if installing_from_iso:
"-drive", f"file={tools_iso},if=none,id=tools_media,media=cdrom,readonly=on", "-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", "-device", "ide-cd,drive=tools_media,bus=ide.3,bootindex=4",
] ]
if os.access("/dev/kvm", os.R_OK | os.W_OK):
qemu_args += ["-enable-kvm", "-cpu", "host"]
accelerator = "KVM"
else:
qemu_args += ["-accel", "tcg", "-cpu", "max"]
accelerator = "TCG"
if installing_from_iso: if installing_from_iso:
print(f"Starting Windows 11 automatic installation with {accelerator} acceleration.") print("Starting Windows 11 automatic installation.")
elif installing: elif installing:
print(f"Starting Windows 11 from the QCOW2 base image with {accelerator} acceleration.") print("Starting Windows 11 from the QCOW2 base image.")
else: else:
print(f"Starting Windows 11 normally with {accelerator} acceleration.") print("Starting Windows 11 normally.")
print(f"Account: guofang / password; RDP: localhost:{rdp_port}; SSH: localhost:{ssh_port}", print(f"Account: guofang / password; RDP: localhost:{rdp_port}; SSH: localhost:{ssh_port}",
flush=True) flush=True)