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.