winvm

Installs, provisions, runs and snapshots a Windows 11 build VM on libvirt and QEMU

winvm

winvm runs a local Windows 11 build VM on plain libvirt and QEMU, for a Linux machine that is not the CI farm. It installs Windows unattended, provisions the farm's MSVC toolchain, starts and stops the VM, runs a build in the guest that survives a dropped connection, keeps snapshots, and copies the Windows toolchain out for a cross build on the host.

winvm is the C++ port of the script integration/winvm/winvm, which was deleted on 2026-10-11 after the two drove a real guest the same way (script-ports §SCP-D-11, §SCP-T-8.2). The verbs, their arguments, the options, the exit codes and the ==> progress lines are the script's. The verbs live in the library libwinvm, so kiln and cross-sweep call the same functions. The script stays in the tree until a run on a real guest shows that the two do the same (§SCP-T-8.2).

To set up a VM and use it day to day, follow A local Windows 11 build VM.

Usage

winvm [options] check                    host requirements, loudly; changes nothing
winvm [options] fetch                    download and verify the ISOs and installers
winvm [options] create                   install Windows unattended, snapshot "installed"
winvm [options] provision                install the pinned toolchain, snapshot "provisioned"
winvm [options] start | stop | status
winvm [options] ssh [command...]         key auth, stock cmd.exe on the other end
winvm [options] run <label> <command>    run it detached: it survives an ssh drop
winvm [options] wait <label> [minutes]   block until that command ends; its exit code
winvm [options] screenshot <file.png>    what the console shows now
winvm [options] sysroot <folder>         copy the toolchain of a host cross build into <folder>
winvm [options] runtime <sysroot>        add what a Windows package carries to a sysroot
winvm [options] hostqt                   fetch this host's Qt tools of qt_version into cross_host_qt
winvm [options] snapshot list
winvm [options] snapshot save <name>     the VM must be shut off
winvm [options] snapshot restore <name>  the VM must be shut off; discards the changes since

The options come before the verb. Everything after the verb belongs to the verb, so winvm ssh dir /s gives /s to the guest.

Option Meaning
--config <file> Read this file in place of integration/winvm/winvm.conf
--root <folder> The checkout. It holds the guest scripts and the vendored provisioning script. Default: the checkout that holds the working folder, else the one that holds the program
--dry-run Print each external command line and each file change, and change nothing. See Dry run
--dry-state <state> The domain that a dry run starts from: undefined, off (the default) or running
-h, --help, help The usage text

Build and run it from the repository root:

qbs run -p winvm -- status

winvm runs on Linux. On another system it exits 2 and does nothing, except with --dry-run.

Verbs

Verb What it does
check Checks the host and changes nothing: /dev/kvm, membership of group kvm, the programs, libvirt, the free memory, the processors, the free disk space and the three ports. Prints one line per check and a count of failures and warnings
fetch Downloads what is missing into cache/ and checks each file. The pinned files are checked against their SHA-256. The Windows ISO has no published digest: the digest of the first download goes into cache/SHA256SUMS, and each later use must match it
create Refuses an existing domain or disk. Runs check quietly, then fetch. Makes the SSH key if it is missing, builds the answer CD with a random guest password, makes the disk, defines and starts the domain, and waits up to 120 minutes for the guest to answer over SSH. Then it shuts the guest down, ejects the install media and saves snapshot installed
provision Starts the VM if it is off. Checks the blob id of the vendored provision-vm.ps1, applies the local patches, copies the scripts into the guest and runs them. The output goes to the terminal and to state_dir/provision.log. Then it restarts the guest and saves snapshot provisioned, unless that snapshot exists
start Applies vcpus and memory_gib to the defined domain if they changed, refuses to start while less memory than memory_gib is available, starts the domain and waits up to 10 minutes for SSH
stop Shuts the guest down through ACPI and waits up to 10 minutes
status The state of the domain, the disk use, the SSH and share settings, the provisioned toolchain and the snapshots
ssh SSH into the guest with the VM's key. The terminal stays attached: standard input, output and error all pass through
run Writes <share>/run/<label>.cmd and starts it in the guest outside the SSH session. The output goes to <share>/logs/<label>.log, the exit code to <share>/logs/<label>.rc. A label is letters, digits, _ and -
wait Waits for <label>.rc, up to minutes (default 720), polling every 20 seconds, and exits with the code in it
screenshot Writes the console to a PNG file
sysroot Copies MSVC, the Windows SDK, Qt's MSVC kit and OpenSSL out of the guest in clang's /winsysroot layout, gives each name with a capital a lowercase link, and writes the kit's target_qt.conf and qmake6 for cross_host_qt
runtime Moves Qt's DLLs, plugins, resources and translations, the Visual C++ redistributable and OpenSSL's DLLs out of the guest into a sysroot. A file that is already there stays as it is
hostqt Downloads qtbase, ICU and qtshadertools of qt_version for this host, checks each against its published SHA-1 and unpacks them into cross_host_qt
snapshot list, save <name> or restore <name>. See Snapshots

Exit codes

Exit Meaning
0 Done
1 A failure that winvm found, with a message on stderr. check exits 1 when a check failed
2 No verb, a bad --dry-state, or not Linux. Nothing was attempted
The program's code An external program that had to succeed failed. The message names its command line. 127 is a program that is not installed
The guest's code wait exits with the code of the guest command. ssh exits with the code of the remote command, or 255 when ssh itself failed
130 A caller of the library cancelled a wait

Output

winvm writes its product to stdout: the ==> progress lines, the report of check, status and snapshot list, and the lines of a dry run. It writes a failure to stderr as winvm: <message>. The output of the programs it starts, such as the progress of curl, goes straight to the terminal.

Configuration

integration/winvm/winvm.conf holds the whole shape of the VM, as key = value lines. A blank line and a line that starts with # are skipped. A value runs to the end of the line, without the spaces around it. Every key is required. Nothing is read from an environment variable.

Key What it is Rule
name The libvirt domain
state_dir Everything the VM owns. Keep it outside every git worktree Absolute, at least one folder below /
vcpus The guest's processors, two threads per core A whole number, even
memory_gib The guest's memory A whole number
disk_gib The size the sparse disk can grow to A whole number
libvirt_uri qemu:///session, so nothing needs root
ssh_port, rdp_port, vnc_port Host ports on 127.0.0.1 Whole numbers
share_dir The shared folder Absolute
share_tag The virtiofs tag of the share
guest_user, guest_hostname The guest account and the computer name
ssh_key The private key. A ~ at the start is your home folder Made by create when it is missing
iso_source evaluation or consumer One of the two
iso_eval_url, iso_eval_image The evaluation ISO and its image name
iso_consumer_language, iso_consumer_image, iso_consumer_key The retail ISO, its image and the generic key that selects it
virtio_win_url, virtio_win_sha256 The virtio drivers
winfsp_url, winfsp_sha256 WinFsp, for the share
openssh_msi_url, openssh_msi_sha256 The fallback OpenSSH Server
qt_version, qbs_version, msvc_toolset The toolchain that provision installs
cross_host_qt Where hostqt puts the host's Qt tools Absolute, at least one folder below /
qt_download_base The Qt online repository of hostqt

A file that breaks a rule is refused with the file, the line where there is one, and the rule, for example winvm.conf:12: unknown key 'colour' or winvm.conf: vcpus must be even (two threads per core).

State layout

<state_dir>/
  cache/            ISOs and installers, with SHA256SUMS
  disks/            current.qcow2, the writable top layer
  nvram/  tpm/      UEFI variables and TPM state
  snapshots/        one folder per snapshot: disk.qcow2, VARS.fd, tpm/, created
  secrets/          guest password, answer CD, known_hosts (mode 700)
  domain.xml        the definition given to libvirt
  provision-stage/  the scripts that provision copies into the guest
  provision.log, provisioned.json

Snapshots

A snapshot is a frozen qcow2 layer plus copies of the UEFI variables and the TPM state. Windows ties the three together, so they are saved and restored together. save moves current.qcow2 into the snapshot, makes it read-only and starts a new empty overlay on it. restore makes a new empty overlay on the chosen layer and copies the variables and the TPM state back. Neither copies the disk, and neither deletes a file. A snapshot name is letters, digits, ., _ and -.

Dry run

winvm --dry-run <verb> prints what the verb would do and changes nothing:

$ winvm --dry-run --dry-state running stop
+ virsh -q -c qemu:///session dominfo octomy-win11
+ virsh -q -c qemu:///session dominfo octomy-win11
+ virsh -q -c qemu:///session domstate octomy-win11
==> Shutting 'octomy-win11' down
+ virsh -q -c qemu:///session shutdown octomy-win11 --mode acpi
+ virsh -q -c qemu:///session dominfo octomy-win11
+ virsh -q -c qemu:///session domstate octomy-win11

A line that starts with + is an external command, quoted as a shell would read it. A line that starts with # is a file change. The dry run keeps a model of the domain: define makes it shut off, start makes it running, shutdown shuts it off. ssh answers 0. A wait whose .rc file is missing ends at once. A verb that needs an answer from the network, such as hostqt, stops where the answer is missing.

What it starts

winvm starts virsh, qemu-img, ssh, scp, ssh-keygen, xorriso, curl, patch and 7z, each by its absolute path and never through a shell. libvirt starts qemu-system-x86_64, passt, virtiofsd and swtpm. The JSON, the digests, the disk use, the free space, the memory and the port checks are done in the program.

The library

libwinvm (src/libs/libwinvm/) holds the configuration (winvm::Config), the renderers of the domain, the answer file and the run script, and each verb as a function of winvm::Vm. The paths of the external programs are one winvm::Programs value, and the programs start through a winvm::Runner. A test gives the paths of program-stub copies, or a runner that records each command, so testWinvm runs create, snapshot save and snapshot restore end to end without a VM.

In this section
Topics
reference tools windows libvirt qemu build
See also