Local Windows 11 build VM
A Windows 11 VM on your own Linux machine with the farm's exact toolchain
A local Windows 11 build VM (libvirt)
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 script does the setup, integration/winvm/winvm, and one file holds
the configuration, integration/winvm/winvm.conf.
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 | integration/devsync 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:
/build/win11-vm/
cache/ ISOs and installers, with SHA256SUMS
disks/ current.qcow2, the writable top layer
nvram/ tpm/ UEFI variables and TPM state
snapshots/ one directory per snapshot: frozen disk layer, VARS.fd, tpm/
secrets/ guest password, answer CD, known_hosts (mode 700)
share/ the shared folder
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:
$ integration/winvm/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
integration/winvm/winvm create # unattended install, then snapshot "installed"
integration/winvm/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
integration/winvm/winvm start
integration/devsync win11vm --verify # committed HEAD -> C:/octomy
integration/winvm/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"
integration/winvm/winvm wait fullbuild # exits with the build's own code
integration/winvm/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.
devsync and devnode reach the VM through its win11vm entry in
integration/devnodes.json, which names the loopback port and the key.
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 (devsync 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.
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
A snapshot is a frozen qcow2 layer plus copies of the UEFI variables and the TPM state. Windows binds those to the disk, so the three are restored together. Saving freezes the current layer and starts a new overlay on it. Restoring recreates the overlay on the chosen layer. Neither step copies the disk or deletes a file, so both finish in well under a second (45 ms measured). The VM must be shut off for either.
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 devsync.
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.