A local Windows 11 build VM
A Windows 11 VM on your own Linux machine with the farm's exact toolchain
Run MSVC builds on your own Linux machine in a Windows 11 VM that gets
the same toolchain as the farm and mathilde64: MSVC 14.44.35207,
Qt 6.10.1 msvc2022_64, qbs 3.3.1 in C:\qbs, ccache, and profile qt6.
One tool does the setup, winvm in src/tools/winvm, and one file holds
the configuration, integration/winvm/winvm.conf. Build it with
kiln build src/tools/winvm and run it from the repository root as
debug/winvm/winvm <verb> (Invoking a tool);
the commands below write winvm <verb> for short.
Every verb, option, configuration key and exit code, the state layout and
the dry run are in the winvm reference.
This page is the procedure.
With kiln, the default build path, the host builds for Windows with
kiln build --os windows and the VM only runs what the host built
(Every suite, built on the host). The MSVC
build in the guest, with qbs build, is the qbs path, the second path until
qbs retires.
This is the route for developer machines. The CI farm keeps its
dockurr/windows container, described in the main page; the
two share nothing except the provisioning script.
How it fits together
| Part | Choice | Why |
|---|---|---|
| Hypervisor | libvirt session instance (qemu:///session) |
QEMU, swtpm, passt and virtiofsd all run as you. Nothing needs root after the package install, and nothing the VM owns needs sudo to remove |
| Firmware | OVMF with Secure Boot and Microsoft keys, TPM 2.0 from swtpm | Windows 11's own requirements, met rather than bypassed |
| Disk | Sparse qcow2 on virtio, discard=unmap |
Grows as it fills, and shrinks again when Windows trims |
| Network | passt user-mode networking, host ports on 127.0.0.1 | No bridge, no firewall rules, no Docker NAT to masquerade |
| Shared folder | virtiofs, seen by Windows through WinFsp | Build artifacts land on the host as ordinary files you own |
| Source | fleet-ctl node sync win11vm, a real git checkout |
Shared folders have lost files under builds before; git gives provenance |
| Toolchain | The farm's provision-vm.ps1, vendored and pinned |
What mathilde64 was provisioned with, then checked against toolchain-requirements.json |
Everything the VM owns lives in state_dir, /build/win11-vm by default,
outside every git worktree. The shared folder is share_dir,
/build/win11-vm/share. The reference lists what is in each folder:
State layout.
Requirements
| Need | Default | Notes |
|---|---|---|
| KVM | /dev/kvm |
Your user must be in group kvm. The desktop session gets access through a logind ACL, but SSH logins and timers do not |
| CPU | 16 vCPUs | Must be even, and leave threads for the host |
| RAM | 32 GiB | winvm start refuses while less than that is available. Freed guest memory goes back to the host (free page reporting) |
| Disk | 250 GiB, sparse | Plan on about 120 GiB actually used for Windows, the toolchain and one LLVM build |
| Ports | 2224 SSH, 3390 RDP, 5911 VNC, all on 127.0.0.1 | Chosen not to collide with the farm container's 2223 and 3389 |
| Packages | Debian 13 | See below |
sudo apt-get install --no-install-recommends \
qemu-system-x86 qemu-utils libvirt-daemon-system libvirt-clients \
ovmf swtpm swtpm-tools passt virtiofsd xorriso curl jq openssh-client patch git
sudo usermod -aG kvm "$(id -un)" # then log out and back in
winvm check checks all of this and changes nothing:
$ winvm check
==> Host requirements for 'octomy-win11' (.../integration/winvm/winvm.conf)
ok /dev/kvm is usable
ok packages present
ok libvirt answers on qemu:///session
...
0 failure(s), 0 warning(s)
Which Windows
iso_source in winvm.conf picks the image:
evaluation(default): Windows 11 Enterprise Evaluation, from a stable official Microsoft link. It always downloads unattended. It expires 90 days after install:slmgr /rearminside the guest extends it, and a reinstall resets it.consumer: the retail multi-edition ISO, installed as Pro with the generic key. It runs unactivated with no expiry. It is fetched through Microsoft's download API, the same waydockurr/windowsdoes it, and Microsoft's bot filter rejects that from some networks. When that happens,winvmstops and prints the manual route: download the ISO in a browser and save it ascache/win11-consumer.iso.
The Windows ISO has no published digest, so its SHA-256 is recorded the
first time it is downloaded and checked on every later use. virtio-win,
WinFsp and the OpenSSH MSI are pinned by digest in winvm.conf.
Setup
winvm create # unattended install, then snapshot "installed"
winvm provision # toolchain, then snapshot "provisioned"
On webstep9 (2026-09-17, Enterprise Evaluation build 26200, 16 vCPUs),
create took 21 minutes, including Windows' own update check during
setup, and provision took 17 minutes.
create downloads what is missing, builds an answer CD with a random
guest password and your public key, defines the domain and starts the
install. It returns once the guest answers over SSH. A first logon script,
guest/firstlogon.ps1, installs:
- the virtio guest tools
- WinFsp, with the virtiofs service set to start automatically
- OpenSSH Server, from the Windows optional feature, or from the pinned MSI if Windows Update cannot be reached
sshd then allows keys only, the key file's ACL is set by SID, and
DefaultShell is left unset, so a session gets stock cmd.exe as in CI.
To watch progress, connect a VNC viewer to 127.0.0.1:5911, or run
winvm screenshot /tmp/win.png.
provision copies the vendored provision-vm.ps1 into the guest and
runs it x64-only. It then fails unless the result matches:
- the qbs and Qt floors, and the qbs known-bad list, in
integration/toolchain-requirements.json - the exact
qt_version,qbs_versionandmsvc_toolsetinwinvm.conf - ccache, git, the
qt6profile andC:\build-environment.jsonall present
It writes the versions to C:\octomy-vm\provisioned.json, with a copy in
state_dir, then reboots so the installers' PATH changes reach sshd.
Re-running it is safe.
Daily use
winvm start
fleet-ctl node sync win11vm --verify # committed HEAD -> C:/octomy
winvm run fullbuild "C:\qbs\bin\qbs.exe build -d C:\build -f C:\octomy\project.qbs profile:qt6 config:release -j 8 --job-limits linker:2 --keep-going"
winvm wait fullbuild # exits with the build's own code
winvm stop
Start it to build, and stop it when the build is done. A running guest holds its memory whether it is building or idle, and on a machine shared with other builds that memory is theirs. Give the VM what a resolve needs (32 GiB) while it runs rather than a smaller size it keeps all day.
Run long builds with run, not ssh. Windows OpenSSH kills everything a
session started when the connection drops, so an ssh "qbs build …" dies with
its connection — including when the guest reboots under it. run starts the
command outside that session, writes its output to
/build/win11-vm/share/logs/<label>.log while it runs, and leaves the exit
code in <label>.rc; wait blocks on it and returns that code. Use ssh for
short commands.
The VM does not update itself. Windows Update is set to notify only, its
automatic reboot is off, and wuauserv and UsoSvc are disabled
(guest/firstlogon.ps1). On 2026-09-18 an automatic update rebooted the VM ten
minutes into a full build and threw away a 98-minute resolve. Update it
deliberately instead: re-enable the two services, install what you want, and
disable them again. Snapshot first.
fleet-ctl node reaches the VM through its win11vm entry in
~/.config/octomy/devnodes.json, which names the loopback port and the key.
If you do not have that file yet, copy integration/devnodes.example.json
there. The template already holds the win11vm entry.
Anything written to the share drive in the guest shows up in
/build/win11-vm/share on the host, owned by you. The share's letter
is the first one free counting down from Z:.
For a desktop, RDP to 127.0.0.1:3390. The account is octomy, and the
password is in secrets/guest-password.
Every suite, built on the host
cross-sweep (src/tools/cross-sweep) runs the whole Windows test set the
way kiln's Windows gate was answered (kiln testing.md §KN-V6): the host
cross-builds twelve suites at a time with kiln --os windows build into the
share (kiln-file §KN-K26), while the guest runs the previous twelve with
kiln test --prebuilt, and each batch's programs are deleted once they have
run. It starts the VM, and stops it on every way out it controls, an error or
Ctrl-C included.
cross-sweep --kiln <kiln> --broker <broker> --variant release --sweep rel1 \
--guest-path 'C:\Qt\6.10.1\msvc2022_64\bin,C:\Qt\Tools\OpenSSLv3\Win_x64\bin' \
--reference /build/win11-vm/share/logs/kw16-msvc.txt,/build/win11-vm/share/logs/kw24-msvc.txt
The guest's checkout must be the host's commit (fleet-ctl node sync win11vm first), and its
kiln is Z:\kiln-win\debug\kiln\kiln.exe unless --guest-kiln names
another. The sweep's folder is /build/win11-vm/share/sweeps/<name>: the
suite list, each batch's build log and guest log, each suite's tests.json,
and verdicts.tsv, kiln's verdict beside the MSVC run's for every suite with
a count per outcome. --from <batch> resumes, and --table-only rewrites the
table from the reports already there.
cross-sweep drives the VM through libwinvm, the library of the winvm
tool. It reads the VM and its share from integration/winvm/winvm.conf of the
checkout. To use another file, give --winvm-config <file>.
The guest's kiln has to know every key the checkout's .kiln files use: one
older than a new key refuses the description, and every suite of the sweep
comes back without a result. Build it from the same commit on the host first,
with kiln --os windows build --variant debug src/tools/kiln.
Windows packages, made on the host
kiln package zip, kiln package msix and kiln package setup make the
build farm's three Windows packages from a Windows build on the host
(kiln-file §KN-K30). The zip needs no VM: kiln reads which DLLs and Qt plugins
the programs need and copies them from the sysroot. That needs the runtime in
the sysroot as well as the toolchain. A sysroot made by winvm sysroot before
2026-10-09 has only the toolchain, so add the runtime to it once:
winvm runtime /build/winsysroot
This copies Qt's DLLs, plugins, resources and translations, the Visual C++
redistributable and OpenSSL's DLLs out of the VM, without their symbol files.
The msix and the installer are packed in the VM, by the Windows SDK's
makeappx and Inno Setup's ISCC. kiln starts the VM for that and stops it
again if it was off.
A packaged application starts only in a logged-on desktop session, which a
winvm run command does not have. Since 2026-10-09 the VM logs the octomy
user on at the console (Winlogon's AutoAdminLogon, with the password from
secrets/guest-password), and a check that starts an msix application runs
from a scheduled task with the interactive flag (schtasks /IT).
Snapshots
winvm stop
winvm snapshot save before-llvm
winvm snapshot restore provisioned # back to a clean toolchain
winvm snapshot list
The VM must be shut off for either step. Neither step copies the disk or deletes a file, so both finish in well under a second (45 ms measured). The reference says what a snapshot holds and what each step does: Snapshots.
Troubleshooting
Links fail with C1002: compiler is out of heap space in pass 2, then
the disk fills. With /GL, every app and test executable does its code
generation at link time, and -j 12 lets twelve of those run together in
24 GiB. On 2026-09-25 they paged for three hours, ten failed with C1002 /
LNK1257, and the pagefile grew from its 32 GB base to its 64 GB ceiling,
which left C: with 0.6 GB and failed the next build with LNK1180: insufficient disk space. Cap the links rather than the compiles:
qbs build … -j 8 --job-limits linker:2. Windows returns a grown pagefile
to its base size only at boot, so winvm stop then winvm start gets the
32 GB back.
A resolve seems to hang, and SSH times out. A resolve of the whole tree
needs about 32 GB, as it does on Linux. With memory_gib = 24, the setting until 2026-09-25
on webstep9, qbs pages. Measured 2026-09-25: 32 GB private and 13 GB
resident, with its main thread waiting on PageIn, a few CPU-seconds a
minute, and an sshd too starved to finish the banner exchange. It is still
making progress, so let it run. The QEMU guest agent still answers while
sshd does not (the qemu-agent-command route under "create times out"; guest-exec runs a command), and
(Get-Process qbs).Threads | Group-Object WaitReason tells paging from a
hang. memory_gib = 32, the current setting, avoids it.
create times out. Take a screenshot. If first logon failed, the
guest has C:\octomy-vm\failed.txt and C:\octomy-vm\firstlogon.log.
Read them over VNC, or without SSH through the QEMU guest agent, which the
virtio guest tools install:
virsh -c qemu:///session qemu-agent-command octomy-win11 '{"execute":"guest-file-open","arguments":{"path":"C:\\octomy-vm\\firstlogon.log"}}',
then guest-file-read on the handle it returns.
provision says the MSVC toolset does not match. The Build Tools
bootstrapper installs the newest 17.x toolset. Don't just move the pin in
winvm.conf: the pin is what makes results comparable with mathilde64.
Move it only together with the farm.
SSH says the host key changed. That happens after a reinstall. Clear
the old key with ssh-keygen -R '[127.0.0.1]:2224' for fleet-ctl node.
winvm's own known_hosts file is reset by create.
Start over.
virsh -c qemu:///session destroy octomy-win11
virsh -c qemu:///session undefine --nvram octomy-win11
Then remove disks/, nvram/, tpm/ and snapshots/ under state_dir
by hand. winvm never deletes them itself.
Where things come from
integration/winvm/vendor/monolith/PROVENANCE.md: which monolith commit the provisioning script is, and why it carries a patch.integration/winvm/winvm.conf: every version, URL and digest. Each key and its rule is in thewinvmreference.