Load the Debian 13 VM hardware from QEMU config files

Move the Debian launcher's QEMU arguments into qemu/debian13.ini, which
the launcher renders with the per-run file paths and passes to QEMU with
-readconfig, followed by qemu/debian13-<arch>.ini for the machine type
and arm64-only devices. Memory, CPU count, SSH port and accelerator are
now set in the config files, replacing VM_MEMORY, VM_CPUS and
VM_SSH_PORT; VM_QEMU_CONFIG selects a different shared config file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKxXLyiL7XJ3a9e8HmR2my
This commit is contained in:
GuofangandClaude Opus 5.5 committed 2026-09-30 10:26:28 +08:00
1 parent 280abbd2de
commit fe8b6f93ff
5 files changed
+141 -47

No files matched your search

+12 -2
View File
@@ -22,8 +22,18 @@ background while still showing the graphical window:
The launcher uses KVM when `/dev/kvm` is accessible, forwards host port `2222` The launcher uses KVM when `/dev/kvm` is accessible, forwards host port `2222`
to guest port `22`, and creates `.vm/debian13/disk.qcow2` as a 20G writable overlay with the to guest port `22`, and creates `.vm/debian13/disk.qcow2` as a 20G writable overlay with the
original qcow2 image as its backing file. The base image therefore stays original qcow2 image as its backing file. The base image therefore stays
unchanged. Resource settings can be overridden with environment variables; unchanged. Run `./run-debian13.py --help` for the supported environment
run `./run-debian13.py --help` for the full list. variables.
The Debian VM hardware (accelerator, 8G memory, 8 CPUs, drives, network) is
defined in the QEMU config file `qemu/debian13.ini`; the machine type and
architecture-specific devices are in `qemu/debian13-amd64.ini` and
`qemu/debian13-arm64.ini`. Edit these files to change the settings. The
launcher fills in the `${...}` file path placeholders of `qemu/debian13.ini`,
writes the result to `.vm/debian13/qemu.ini` and starts QEMU with
`-readconfig` for that file followed by the one for the guest architecture.
QEMU arguments given after `--` override the config files, for example
`./run-debian13.py -- -m 4G -smp 4`.
The launchers accept `--state-dir DIR` to keep The launchers accept `--state-dir DIR` to keep
the VM disk, seed ISO, UEFI variables, PID file and console log in `DIR` the VM disk, seed ISO, UEFI variables, PID file and console log in `DIR`
+4
View File
@@ -0,0 +1,4 @@
# amd64 part of the Debian 13 QEMU configuration; read after debian13.ini.
[machine]
type = "q35"
+17
View File
@@ -0,0 +1,17 @@
# arm64 part of the Debian 13 QEMU configuration; read after debian13.ini.
[machine]
type = "virt"
# Unlike q35, the "virt" machine has no default GPU or USB input devices.
[device "gpu0"]
driver = "virtio-gpu-pci"
[device "usb0"]
driver = "qemu-xhci"
[device "kbd0"]
driver = "usb-kbd"
[device "tablet0"]
driver = "usb-tablet"
+69
View File
@@ -0,0 +1,69 @@
# QEMU configuration for the Debian 13 virtual machine, shared by all
# architectures. The machine type and architecture-specific devices are in
# debian13-<arch>.ini, which QEMU reads after this file.
#
# run-debian13.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, daemon mode) are still added
# by the launcher.
[name]
guest = "debian13"
# 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}"
# Writable qcow2 overlay on top of the shared base image.
[drive "disk0"]
if = "none"
format = "qcow2"
file = "${disk_image}"
[device "disk0-dev"]
driver = "virtio-blk-pci"
drive = "disk0"
# cloud-init seed ISO.
[drive "seed0"]
if = "none"
format = "raw"
readonly = "on"
file = "${seed_image}"
[device "seed0-dev"]
driver = "virtio-blk-pci"
drive = "seed0"
# User-mode networking with guest SSH forwarded to the host.
[netdev "net0"]
type = "user"
hostfwd = "tcp::2222-:22"
[device "nic0"]
driver = "virtio-net-pci"
netdev = "net0"
+39 -45
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
@@ -53,9 +54,6 @@ else:
state_dir = Path(env("VM_STATE_DIR") or script_dir / ".vm" / "debian13") state_dir = Path(env("VM_STATE_DIR") or script_dir / ".vm" / "debian13")
disk_size = env("VM_DISK_SIZE") or "20G" disk_size = env("VM_DISK_SIZE") or "20G"
memory = env("VM_MEMORY") or "8G"
cpus = env("VM_CPUS") or "8"
ssh_port = env("VM_SSH_PORT") or "2222"
def normalize_arch(arch): def normalize_arch(arch):
@@ -68,12 +66,9 @@ def normalize_arch(arch):
native_arch = normalize_arch(platform.machine()) native_arch = normalize_arch(platform.machine())
host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch) host_arch = normalize_arch(env("VM_IMAGE_ARCH") or native_arch)
if host_arch == "arm64": qemu_binary = "qemu-system-aarch64" if host_arch == "arm64" else "qemu-system-x86_64"
qemu_binary = "qemu-system-aarch64" qemu_config_template = Path(env("VM_QEMU_CONFIG") or script_dir / "qemu" / "debian13.ini")
machine_type = "virt" qemu_arch_config = script_dir / "qemu" / f"debian13-{host_arch}.ini"
else:
qemu_binary = "qemu-system-x86_64"
machine_type = "q35"
def pick_firmware(override, *candidates): def pick_firmware(override, *candidates):
@@ -125,7 +120,10 @@ Usage: {Path(sys.argv[0]).name} [--daemon] [--state-dir DIR] [-- QEMU_ARGUMENT .
Start the Debian 13 virtual machine. A writable overlay is created in the state Start the Debian 13 virtual machine. A writable overlay is created in the state
directory (default: .vm/debian13/), so the downloaded base image is never directory (default: .vm/debian13/), so the downloaded base image is never
modified. modified. The VM hardware (memory, CPUs, SSH port forward, ...) is defined in
qemu/debian13.ini, which is rendered to qemu.ini in the state directory and
loaded with QEMU's -readconfig, followed by qemu/debian13-<arch>.ini. Later
QEMU arguments override them, e.g. `-- -m 4G -smp 4`.
Options: Options:
--daemon Run in the background while keeping the graphical window --daemon Run in the background while keeping the graphical window
@@ -134,9 +132,6 @@ Options:
-h, --help Show this help -h, --help Show this help
Environment variables: Environment variables:
VM_MEMORY Guest memory (default: 8G)
VM_CPUS Virtual CPU count (default: 8)
VM_SSH_PORT Host port forwarded to guest SSH (default: 2222)
VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper) VM_BASE_IMAGE Base qcow2 image path (default: resolved by shared image helper)
VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk) VM_SEED_IMAGE cloud-init seed ISO path (default: alongside the VM disk)
VM_STATE_DIR Runtime state directory (default: .vm/debian13) VM_STATE_DIR Runtime state directory (default: .vm/debian13)
@@ -146,6 +141,8 @@ Environment variables:
VM_UEFI_CODE UEFI read-only firmware path (auto-detected for the selected architecture) VM_UEFI_CODE UEFI read-only firmware path (auto-detected for the selected architecture)
VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture) VM_UEFI_VARS UEFI variable-store template path (auto-detected for the selected architecture)
VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere) VM_DISPLAY QEMU display backend (default: cocoa on macOS, gtk elsewhere)
VM_QEMU_CONFIG QEMU config template rendered into the state directory and
passed to -readconfig (default: qemu/debian13.ini)
""") """)
@@ -176,6 +173,7 @@ seed_image = Path(env("VM_SEED_IMAGE") or disk_image.parent / "seed.iso")
uefi_vars = state_dir / "OVMF_VARS_4M.fd" uefi_vars = state_dir / "OVMF_VARS_4M.fd"
pid_file = state_dir / "qemu.pid" pid_file = state_dir / "qemu.pid"
log_file = state_dir / "console.log" log_file = state_dir / "console.log"
qemu_config = state_dir / "qemu.ini"
for command_name in (qemu_binary, "qemu-img"): for command_name in (qemu_binary, "qemu-img"):
if shutil.which(command_name) is None: if shutil.which(command_name) is None:
@@ -232,42 +230,38 @@ if not disk_image.is_file():
is_macos = platform.system() == "Darwin" is_macos = platform.system() == "Darwin"
display_backend = env("VM_DISPLAY") or ("cocoa" if is_macos else "gtk") display_backend = env("VM_DISPLAY") or ("cocoa" if is_macos 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_flash,
"uefi_vars": uefi_vars,
"disk_image": disk_image,
"seed_image": seed_image,
}
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", "debian13", "-readconfig", str(qemu_config),
"-machine", machine_type, "-readconfig", str(qemu_arch_config),
# "max" is the host CPU under KVM/HVF and the fullest model under TCG.
"-cpu", "max",
"-display", display_backend, "-display", display_backend,
"-m", memory,
"-smp", cpus,
"-drive", f"if=pflash,format=raw,readonly=on,file={uefi_code_flash}",
"-drive", f"if=pflash,format=raw,file={uefi_vars}",
"-drive", f"file={disk_image},if=none,id=disk0,format=qcow2",
"-device", "virtio-blk-pci,drive=disk0",
"-drive", f"file={seed_image},if=none,id=seed0,format=raw,readonly=on",
"-device", "virtio-blk-pci,drive=seed0",
"-nic", f"user,model=virtio-net-pci,hostfwd=tcp::{ssh_port}-:22",
] ]
if machine_type == "virt": print(f"Starting {host_arch} VM from {qemu_config_template} "
# Unlike q35, the "virt" machine has no default GPU or USB input devices. f"and {qemu_arch_config}.", flush=True)
qemu_args += [
"-device", "virtio-gpu-pci",
"-device", "qemu-xhci",
"-device", "usb-kbd",
"-device", "usb-tablet",
]
if is_macos and host_arch == native_arch:
qemu_args += ["-accel", "hvf", "-cpu", "host"]
accelerator = "HVF"
elif host_arch == native_arch and 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"
print(f"Starting {host_arch} VM with {accelerator} acceleration; "
f"SSH is forwarded to localhost:{ssh_port}.", flush=True)
if daemonize: if daemonize:
log_file.write_text("") log_file.write_text("")