winvm 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. winvm-1.2.0/LICENSE +21 -0
  2. winvm-1.2.0/PKG-INFO +410 -0
  3. winvm-1.2.0/README.md +394 -0
  4. winvm-1.2.0/pyproject.toml +28 -0
  5. winvm-1.2.0/setup.cfg +4 -0
  6. winvm-1.2.0/src/winvm/__init__.py +2 -0
  7. winvm-1.2.0/src/winvm/__main__.py +5 -0
  8. winvm-1.2.0/src/winvm/balloon.py +132 -0
  9. winvm-1.2.0/src/winvm/build.py +406 -0
  10. winvm-1.2.0/src/winvm/cli.py +813 -0
  11. winvm-1.2.0/src/winvm/config.py +387 -0
  12. winvm-1.2.0/src/winvm/data/guest/autounattend.xml +17 -0
  13. winvm-1.2.0/src/winvm/data/guest/lists/appx-keep.txt +10 -0
  14. winvm-1.2.0/src/winvm/data/guest/lists/capabilities-remove.txt +23 -0
  15. winvm-1.2.0/src/winvm/data/guest/lists/features-keep-payload.txt +7 -0
  16. winvm-1.2.0/src/winvm/data/guest/lists/features-remove.txt +10 -0
  17. winvm-1.2.0/src/winvm/data/guest/lists/offline-delete.txt +10 -0
  18. winvm-1.2.0/src/winvm/data/guest/lists/offline-reg.txt +136 -0
  19. winvm-1.2.0/src/winvm/data/guest/lists/services-disable.txt +51 -0
  20. winvm-1.2.0/src/winvm/data/guest/payload/SetupComplete.cmd +4 -0
  21. winvm-1.2.0/src/winvm/data/guest/payload/common.ps1 +16 -0
  22. winvm-1.2.0/src/winvm/data/guest/payload/final.ps1 +16 -0
  23. winvm-1.2.0/src/winvm/data/guest/payload/firstlogon.ps1 +60 -0
  24. winvm-1.2.0/src/winvm/data/guest/payload/setupcomplete.ps1 +117 -0
  25. winvm-1.2.0/src/winvm/data/guest/pe.cmd +342 -0
  26. winvm-1.2.0/src/winvm/data/guest/start.cmd +20 -0
  27. winvm-1.2.0/src/winvm/data/guest/unattend.xml.in +65 -0
  28. winvm-1.2.0/src/winvm/data/images/base.yaml +37 -0
  29. winvm-1.2.0/src/winvm/data/winvm.yaml +56 -0
  30. winvm-1.2.0/src/winvm/guest.py +107 -0
  31. winvm-1.2.0/src/winvm/host.py +328 -0
  32. winvm-1.2.0/src/winvm/media.py +129 -0
  33. winvm-1.2.0/src/winvm/store.py +112 -0
  34. winvm-1.2.0/src/winvm/util.py +184 -0
  35. winvm-1.2.0/src/winvm/vm.py +583 -0
  36. winvm-1.2.0/src/winvm.egg-info/PKG-INFO +410 -0
  37. winvm-1.2.0/src/winvm.egg-info/SOURCES.txt +39 -0
  38. winvm-1.2.0/src/winvm.egg-info/dependency_links.txt +1 -0
  39. winvm-1.2.0/src/winvm.egg-info/entry_points.txt +2 -0
  40. winvm-1.2.0/src/winvm.egg-info/requires.txt +2 -0
  41. winvm-1.2.0/src/winvm.egg-info/top_level.txt +1 -0
winvm-1.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shadymeowy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
winvm-1.2.0/PKG-INFO ADDED
@@ -0,0 +1,410 @@
1
+ Metadata-Version: 2.4
2
+ Name: winvm
3
+ Version: 1.2.0
4
+ Summary: Build and run slim, unattended Windows VMs on QEMU/KVM from small YAML image files
5
+ License-Expression: MIT
6
+ Keywords: windows,qemu,kvm,vm,unattended,debloat
7
+ Classifier: Environment :: Console
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Topic :: System :: Emulators
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: PyYAML>=6
14
+ Requires-Dist: pycdlib>=1.22
15
+ Dynamic: license-file
16
+
17
+ # winvm: slim, reproducible Windows VMs from YAML image files
18
+
19
+ `winvm` turns an **official Microsoft Windows ISO** into a minimal Windows VM on
20
+ QEMU/KVM: unattended, reproducible, runs as your user, not activated. Each image is
21
+ described by one small YAML file, like a Dockerfile: built from an ISO, or on top of
22
+ another image. Files refer to each other by name, so bases and images are easy to
23
+ share and reuse.
24
+
25
+ * No Windows Update, Defender, SmartScreen, Copilot/Recall/AI, Edge, OneDrive,
26
+ Store or inbox apps, telemetry, search indexer, hibernation, pagefile or WinRE.
27
+ The component store is reset and payloads of disabled features are stripped.
28
+ * Guest tools: virtio drivers, **qemu-guest-agent**, spice-vdagent (clipboard).
29
+ * **CLI first.** Headless VM with OpenSSH (keys) and PowerShell. `winvm sh <command>`
30
+ runs a guest command like a local one: exit code, stdin, pipes, Unix line endings,
31
+ current folder, in well under a second.
32
+ * **Mounts.** Host folders appear in the guest as `C:\mnt\<name>` or any folder path you
33
+ pick, from the config or added and removed while the VM runs (`winvm mount ~/proj`).
34
+ * **Layers.** Images are zstd-compressed read-only qcow2 layers; instances run on thin
35
+ disks on top. Several instances run at once, each with its own ports.
36
+
37
+ ## Requirements
38
+
39
+ * Linux x86-64 with KVM (`/dev/kvm` usable by you; on most distros: be in group `kvm`)
40
+ * QEMU - the one thing to install:
41
+
42
+ ```sh
43
+ sudo dnf install qemu-kvm # Fedora
44
+ sudo apt install qemu-system-x86 qemu-utils # Debian / Ubuntu
45
+ sudo pacman -S qemu-base # Arch
46
+ ```
47
+
48
+ plus `samba` if you want mounts (host folders in the guest).
49
+ * Python ≥ 3.10, 8 GB RAM or more (a VM takes 4 GB by default), and disk space: about
50
+ 9 GB for the downloads (mostly the Windows ISO) plus about 6 GB per built `base`,
51
+ 25 GB free to be comfortable
52
+
53
+ Everything else winvm needs comes with it or is downloaded once, each file pinned by
54
+ its sha256: the Windows ISO (from Microsoft, into `~/.local/share/winvm/iso`) and, into
55
+ `~/.cache/winvm`, virtio-win drivers, Win32-OpenSSH and, if the host has none, UEFI
56
+ firmware (Debian's OVMF package). winvm asks before downloading (`-y`: don't ask).
57
+ `winvm check` shows what is there and what is missing. winvm runs as your user, never
58
+ as root.
59
+
60
+ ## Install
61
+
62
+ From a checkout of this repository:
63
+
64
+ ```sh
65
+ uv tool install . # or: pipx install . or: pip install --user .
66
+ winvm check # what the host has, what will be downloaded
67
+ ```
68
+
69
+ Shell completion: commands and options come from winvm's own definitions (rerun after
70
+ updates); instance and image names (`-i`, `stop`, `--image`, `rmi`, `reset`, ...) are looked
71
+ up live when you press Tab:
72
+
73
+ ```sh
74
+ winvm completion bash > ~/.local/share/bash-completion/completions/winvm
75
+ winvm completion fish > ~/.config/fish/completions/winvm.fish
76
+ ```
77
+
78
+ ## Quick start
79
+
80
+ ```sh
81
+ winvm up # first time: asks to download, then builds image `base`
82
+ winvm sh # PowerShell in the guest (exit with `exit`)
83
+ winvm sh Get-ComputerInfo -Property OsName,OsBuildNumber
84
+ winvm stop
85
+ ```
86
+
87
+ No config file is needed: `winvm` uses its built-in one, builds image `base` from the
88
+ Windows ISO and starts instance `main` on it. `winvm sh` (or `ssh`, `cp`, `rdp`) also
89
+ creates `main` when it doesn't exist yet, so `winvm sh` alone works from scratch; other
90
+ instance names have to be created with `up` first. The first `up` lists what it needs to
91
+ download, about 8.6 GB in all: the Windows 11 25H2 ISO (English, about 7.7 GB,
92
+ straight from Microsoft's servers) and the virtio drivers (about 0.9 GB), and asks
93
+ once. Have an ISO already, or want another language? Download it from
94
+ <https://www.microsoft.com/software-download/windows11> and use
95
+ `winvm up --iso ~/Downloads/Win11_xxx.iso` instead; it is remembered.
96
+
97
+ **The first build takes a while** - installing and debloating Windows can take up to
98
+ an hour, depending on the disk (an SSD helps a lot) and the CPU, plus the download
99
+ time. It runs unattended; `winvm` shows the progress, and you can watch the VM with
100
+ a VNC viewer at the address it prints. Later builds of the same image are skipped
101
+ (it's cached), and `slim: true` (see below) makes rebuilds much faster.
102
+ From then on:
103
+
104
+ ```sh
105
+ winvm up # seconds until SSH answers
106
+ winvm sh <command> # as often as you like, well under a second each
107
+ winvm stop
108
+ ```
109
+
110
+ `main` keeps everything you do in it across stop/start. `winvm reset` gives it a fresh
111
+ Windows again; `winvm save NAME` freezes its current state as an image to come back
112
+ to or start other instances from:
113
+
114
+ ```sh
115
+ winvm stop # save needs the instance stopped
116
+ winvm save office # main's disk becomes image "office" (compressed, read-only)
117
+ winvm up work --image office # a second instance, "work", starting from it
118
+ winvm -i work sh # -i picks the instance (default: main)
119
+ winvm reset # later: main back to "office", dropping what came after
120
+ ```
121
+
122
+ `main` continues on the saved image, so the image itself never changes. A saved image
123
+ can't be rebuilt (there is no recipe) and stays on the `base` it was made from. To
124
+ bake your own tools into images that rebuild on a newer `base`, write image files
125
+ (next section).
126
+
127
+ ## Images
128
+
129
+ An image is one YAML file; its file name is its name. Either it is built **from an
130
+ ISO** (unattended install + debloat, offline), or **from another image** (boot it, run
131
+ steps with network, keep the changes):
132
+
133
+ ```yaml
134
+ # ~/.config/winvm/images/base.yaml - replaces the built-in base (`winvm config dump base`);
135
+ # an ISO image's file only needs what differs from the built-in base
136
+ edition: Windows 11 Pro N # `winvm editions` lists them
137
+ keyboard: "041f:0000041f" # Turkish Q
138
+ slim: true # cache the debloated image: much faster rebuilds
139
+ setup: # during the install, as SYSTEM, without network
140
+ - download: {url: "https://www.7-zip.org/a/7z2409-x64.msi", sha256: <sha256>}
141
+ - run: Start-Process msiexec -Wait -ArgumentList '/i', '7z2409-x64.msi', '/qn'
142
+ ```
143
+
144
+ ```yaml
145
+ # ~/.config/winvm/images/dev.yaml
146
+ from: base # an image name, or a path: ../shared/base.yaml
147
+ steps: # with network; files land in a work folder
148
+ - download: {url: "https://www.python.org/ftp/python/3.13.1/python-3.13.1-amd64.exe",
149
+ sha256: <sha256>}
150
+ - run: .\python-3.13.1-amd64.exe /quiet InstallAllUsers=1 PrependPath=1 | Out-Null
151
+ - copy: [./dotfiles, 'C:\Users\user\dotfiles'] # ./ = next to this file
152
+ - restart: true
153
+ run: # how instances of dev run (images on dev inherit it)
154
+ ram: 8G
155
+ mounts:
156
+ projects: ~/Projects # guest C:\mnt\projects
157
+ docs: {path: ~/Documents, readonly: true}
158
+ src: {path: ~/src, at: 'C:\src'} # anywhere in the guest instead of C:\mnt
159
+ ```
160
+
161
+ ```sh
162
+ winvm up dev # builds base and dev as needed, runs instance "dev"
163
+ winvm -i work up --image dev --ram 6G # another instance of dev, with its own RAM
164
+ winvm up ./team/office.yaml # an image file anywhere: instance "office"
165
+ ```
166
+
167
+ **Where image files come from.** A name like `dev` means `dev.yaml` in
168
+ `~/.config/winvm/images/` (setting `images_dir`), else a built-in (only `base`). Any
169
+ other file is given by its path once; winvm remembers it, so later commands (and
170
+ `from: NAME` in other files) work by name from any folder. Like a Docker image, a
171
+ built image stays usable by name even when its file is gone (or for images you
172
+ `save`): it just can't be rebuilt without the file. Your `base.yaml` replaces the built-in `base` (`winvm images` says so); two
173
+ files with the same name are an error, never a silent pick. A file's folder is its
174
+ context: `copy: [./dotfiles]` and mount paths are relative to the file. For files
175
+ you share, leave `iso:` out: everyone links their own ISO once with `--iso`.
176
+
177
+ **Steps.** `run` is PowerShell; a thrown error or a non-zero exit fails the build.
178
+ `download` fetches on the host, checks the sha256 (get it with
179
+ `curl -sL URL | sha256sum`) and puts the file into the work folder. `copy: [HOST]`
180
+ puts a file or folder there too, `copy: [HOST, GUEST]` anywhere in the guest
181
+ (`from` images only). `restart` reboots (`from` images only). Run steps start in
182
+ the work folder, which is deleted when the build ends. Files that `setup` steps use
183
+ travel on a CD: their names must be valid on Windows and at most 64 characters long.
184
+
185
+ **Instances.** `winvm up NAME` runs the instance NAME, on the image of the same name
186
+ if there is one, else on `base` (or `--image`). Its settings come from the machine's
187
+ `run` defaults, then the `run:` of its image and the images below, then its own
188
+ `--ram/--cpus` (kept in its folder).
189
+
190
+ **Rebuilds work like Docker layers.** Every image has a content hash over its
191
+ definition, the files it copies and the image below it (not over where its file is).
192
+ Change any of it and the image (and everything built on it) gets a new version on the
193
+ next `build`, `reset`, or `up` of a new instance. Existing instances keep running on
194
+ the version they were created on; `winvm ps` marks them outdated and `winvm reset`
195
+ moves them over (discarding their changes). `winvm prune` deletes versions nothing uses.
196
+
197
+ **Machine settings** (folders, ports, UUID, RAM reserve, default `run`) are in
198
+ `~/.config/winvm/winvm.yaml`, merged over the built-in ones (`winvm config dump`).
199
+ They never change what an image contains, so an image file means the same on every
200
+ machine.
201
+
202
+ ## Running commands in the guest: `winvm sh`
203
+
204
+ ```sh
205
+ winvm sh # interactive shell
206
+ winvm sh ipconfig /all # a command line; PowerShell parses it
207
+ winvm sh 'gci C:\ | select Name' # one quoted argument = PowerShell code as is
208
+ winvm sh notepad "a b.txt" # words with spaces stay one argument
209
+ winvm sh -t python # -t: a terminal, for interactive programs
210
+ cat x | winvm sh findstr foo # stdin reaches native programs ...
211
+ cat x | winvm sh 'stdin | sort' # ... and PowerShell code reads it with `stdin`
212
+ winvm sh ipconfig | grep IPv4 # output has Unix line endings
213
+ winvm sh cmd /c exit 3; echo $? # 3: the exit code is the command's (1 for a failed cmdlet)
214
+ cd ~/Projects/x && winvm -i work sh dir # inside a mount: runs in C:\mnt\projects\x
215
+ ```
216
+
217
+ The command travels base64-encoded, so no quoting is lost between your shell, ssh,
218
+ sshd and Windows; all commands to an instance share one SSH connection. `winvm ssh`
219
+ is plain ssh (`winvm ssh -L 8080:localhost:80`, `winvm ssh hostname`),
220
+ `winvm cp a.txt :C:/Users/user/` is scp, and
221
+ `winvm ga exec PROG [ARGS]` goes through the guest agent without network.
222
+
223
+ ## Dynamic memory
224
+
225
+ ```yaml
226
+ run:
227
+ ram: {min: 2G, max: 8G} # or: winvm up --ram 2G-8G
228
+ ```
229
+
230
+ The VM gets `max`, and winvm keeps it near what the guest actually uses: a small
231
+ helper per instance reads the guest's memory stats (virtio-balloon) and moves the
232
+ balloon - it grows the guest at once when free memory runs short, and gives memory
233
+ back to the host after it has been free for half a minute, step by step. The guest
234
+ also gets a pagefile (added on its first start in this mode, one reboot), which
235
+ absorbs a sudden large allocation while memory flows back in. If the helper stops or
236
+ the guest's stats go silent, the guest gets its full `max` back; `winvm up`/`sh`
237
+ restart a missing helper. `winvm ps` shows the current size (`2.1/8G`); the log is
238
+ `balloon.log` in the instance's run folder.
239
+
240
+ Windows touches all of its RAM at boot, so without this a 4 GB instance takes 4 GB
241
+ of the host as long as it runs. The RAM guard still counts `max` when starting VMs:
242
+ dynamic memory leaves room for the host, it doesn't fit more VMs than `max` allows.
243
+ Several instances on the same image can also share identical memory pages with the
244
+ kernel's KSM (off by default; `winvm check` shows the command to turn it on).
245
+
246
+ ## Mounts
247
+
248
+ ```sh
249
+ winvm mount ~/data # running instance: guest C:\mnt\data, at once
250
+ winvm mount ~/data d2 --ro # another name (C:\mnt\d2), read-only
251
+ winvm mount ~/src 'C:\src' # a Windows path instead of a name: the link goes there
252
+ winvm mount # list
253
+ winvm umount d2 # by name, or where it is: winvm umount 'C:\src'
254
+ ```
255
+
256
+ Mounts from `run:` settings are set up at every start; `mount`/`umount` change a
257
+ running instance until it stops. Each instance runs its own unprivileged Samba server
258
+ (QEMU forwards `\\10.0.2.4` to it), whose config winvm rewrites on the fly. The guest
259
+ gets folder links (in `C:\mnt`, or where `at:` / a Windows path puts them) rather than
260
+ drive letters, because a drive letter exists only in the logon session that mapped it.
261
+ winvm only ever removes links it made itself (the guest keeps a list of them); a
262
+ mount point taken by a real folder or file is refused, never replaced.
263
+
264
+ ## Commands
265
+
266
+ Instance commands act on `-i NAME`, default `main`. Wherever an IMAGE is asked for,
267
+ a name or the path of an image file works. `-y` (before the command) allows downloads
268
+ without asking.
269
+
270
+ | command | |
271
+ |---|---|
272
+ | `up [NAME\|FILE...] [--image IMAGE] [--ram R\|MIN-MAX] [--cpus N] [--iso ISO] [--all] [--gui]` | create (building images as needed) and start; ready when SSH answers |
273
+ | `stop [NAME...] [--all]` / `kill` | ACPI shutdown (hard kill after 120 s) / power off |
274
+ | `ps` | instances: state, ports, disk, RAM, image version, mounts |
275
+ | `sh [-t] [CMD]`, `ssh`, `cp`, `wait` | see above |
276
+ | `mount [PATH [NAME\|WINPATH]] [--ro]` / `umount NAME\|WINPATH` | host folders in the running guest |
277
+ | `gui` / `rdp` | VNC viewer (or start with a GTK window) / FreeRDP session |
278
+ | `ga`, `balloon [SIZE]`, `screenshot`, `cmdline` | guest agent, live RAM, PNG of the screen, QEMU argv |
279
+ | `build [IMAGE...] [--force]` | build missing or changed images (default: all known) |
280
+ | `images` | images: built versions, status, size, base, users, defining file |
281
+ | `save NAME` | freeze the instance's disk as image NAME; the instance continues on it |
282
+ | `reset [IMAGE]` | discard the instance's changes: fresh disk on the current version |
283
+ | `rm` / `rmi IMAGE` / `prune [--cache]` | delete an instance / an image / unused versions and leftovers |
284
+ | `check` | what the host has and lacks, what will be downloaded |
285
+ | `fetch [IMAGE...]` | download now what building needs (asks; `-y`) |
286
+ | `editions [IMAGE]` | editions in an image's Windows ISO |
287
+ | `completion bash\|fish` | print a shell completion script |
288
+ | `config dump [base]`, `config show [IMAGE]`, `config path` | built-in settings or base / what is in effect / files and folders |
289
+
290
+ `save` is for states you produce by hand (e.g. after activating something); an
291
+ instance's disk keeps its changes across stop/start anyway.
292
+
293
+ ## Where things live
294
+
295
+ ```
296
+ ~/.config/winvm/winvm.yaml machine settings (optional)
297
+ ~/.config/winvm/images/NAME.yaml your image files (optional)
298
+ ~/.local/share/winvm/ --data-dir / $WINVM_DATA_DIR / settings.data_dir
299
+ images/base@fb97f6be18d2.qcow2 read-only layers, <name>@<hash>, + .json metadata
300
+ instances/main/disk.qcow2 an instance's own changes, backed by a layer
301
+ iso/ keys/ share/ linked or downloaded ISOs, the SSH key the guests trust, default mount
302
+ ~/.cache/winvm/ --cache-dir / $WINVM_CACHE_DIR / settings.cache_dir
303
+ downloads/ slim/ firmware/ pinned downloads, cached slim images, OVMF if downloaded
304
+ build/ build scratch
305
+ $XDG_RUNTIME_DIR/winvm-*/<name>/ while running: pid, ports, sockets, serial.log, ssh_config, smb/
306
+ ```
307
+
308
+ Backing files are relative paths, so `qemu-img info --backing-chain` shows the
309
+ same chain as `winvm images` and the data folder can be moved.
310
+
311
+ **Access.** SSH `127.0.0.1:2222` (+*n*), user `user`, key `~/.local/share/winvm/keys/id_ed25519`
312
+ (`~/.ssh/id_ed25519.pub` is authorized too). For plain `ssh main` or VS Code
313
+ Remote-SSH, add `Include /run/user/*/winvm-*/*/ssh_config` to `~/.ssh/config`.
314
+ RDP `127.0.0.1:3390`, user `user`, password `winslim`. VNC `127.0.0.1:5930`.
315
+ All forwards bind to 127.0.0.1; the guest firewall is off on purpose.
316
+
317
+ **Host RAM guard.** Before starting a VM, winvm checks `MemAvailable` minus what
318
+ running VMs may still grow into, and refuses when the VM plus `settings.mem_reserve`
319
+ doesn't fit.
320
+
321
+ ## How an ISO image is built
322
+
323
+ ```
324
+ VM without network: CD 1 Microsoft's Windows ISO, unmodified (boots; Enter for "press any key")
325
+ CD 2 winvm payload: autounattend.xml + winslim\ (pe.cmd, lists, unattend.xml,
326
+ first-boot scripts, your setup steps) - written in pure Python
327
+ CD 3 virtio-win (drivers, guest tools)
328
+ ```
329
+
330
+ 1. **WinPE.** Windows Setup finds `autounattend.xml` on CD 2 and runs
331
+ `winslim\start.cmd` → `pe.cmd` (`data/guest/`), which installs Windows itself:
332
+ it refuses to run unless the SMBIOS vendor is QEMU, partitions disk 0 (ESP + MSR +
333
+ C:, no recovery), applies the edition and services it offline with the image's own
334
+ DISM: AppX packages not in `appx-keep.txt`, capabilities, features, payloads,
335
+ ResetBase, registry edits, services, Edge/OneDrive/WinRE files (all in
336
+ `data/guest/lists/`). Then drivers, unattend, first-boot scripts, bcdboot, reboot.
337
+ 2. **Setup/OOBE**, unattended and offline.
338
+ 3. **SetupComplete** (SYSTEM): guest tools, OpenSSH, .NET native images (PowerShell
339
+ starts in 0.2 s instead of 2 s), firewall/power settings, an online debloat
340
+ sweep, your `setup` steps, cleanup and trim.
341
+ 4. **First logon** creates the profile and removes WebView2; one more boot deletes
342
+ what was locked, trims and powers off.
343
+ 5. Host: `qemu-img convert` (zstd) writes the layer.
344
+
345
+ With `slim: true` WinPE instead captures the serviced image right after step 1 onto
346
+ an extra disk, which winvm keeps in `~/.cache/winvm/slim`; from then on builds
347
+ attach it and apply it directly, skipping the servicing. Its cache key is the ISO
348
+ (by sha256), the edition, the debloat lists, pe.cmd and the virtio drivers, so it is
349
+ shared by every image using them.
350
+
351
+ Progress (from step 3 on) streams from the build VM's serial port; inside the guest
352
+ the logs are in `C:\Windows\Setup\winslim*.log`. `WINVM_DEBUG=1 winvm build` makes a
353
+ WinPE failure stay on screen (`winvm -i _install-NAME screenshot`) instead of
354
+ powering off.
355
+
356
+ ## What to expect
357
+
358
+ Measured with Windows 11 Pro 25H2, 4 vCPUs / 4 GB, KVM, on an SSD; times vary with the
359
+ host, so only the relations are worth remembering:
360
+
361
+ * **First build** of `base`: up to about an hour, plus downloading about 8.6 GB.
362
+ A `slim` image's first build captures the debloated image too and takes a bit
363
+ longer; after that, rebuilds of it skip the debloat and take a fraction of the time.
364
+ * **An image `from` another**: about a minute plus whatever its steps do.
365
+ * **Daily use**: an instance boots to SSH in seconds; `winvm sh` commands return in
366
+ well under a second.
367
+ * **Disk**: the base layer is about 5.5 GB (Microsoft's ISO: 7-9 GB); a fresh
368
+ instance disk is under 1 MB and grows with what you change; a cached slim image is
369
+ about 6 GB.
370
+ * **RAM**: an idle headless guest uses about 1 GB inside, but Windows touches all of
371
+ its RAM at boot, so the host gives it the full `ram`; `winvm balloon 2G` hands the
372
+ rest back while it runs.
373
+
374
+ CompactOS (`debloat.compact_os`) reaches only ~1.8:1 and its output doesn't
375
+ compress further, so raw guest data plus qcow2 zstd is smaller and the default.
376
+
377
+ ## Activation, Office, re-enabling things
378
+
379
+ The VM is unactivated; Windows is Microsoft software under Microsoft's license
380
+ terms, and activating it needs a license of your own. Every VM has the same SMBIOS
381
+ UUID (`settings.uuid`), so all images and instances look like one machine to
382
+ activation: activate in an instance and `winvm save` it, and rebuilding or resetting
383
+ keeps it. That is meant for one licensed machine: running several activated copies
384
+ at the same time needs a license for each (give them different `uuid`s via
385
+ separate data dirs). `slmgr /ipk <key>` then `slmgr /ato`, or Settings › Activation.
386
+
387
+ Office installs fine as an image step or by hand with the [Office Deployment Tool][odt],
388
+ which downloads from Microsoft's CDN, not Windows Update. Microsoft 365 Apps need
389
+ the WebView2 runtime (`debloat.remove_webview: false`, or `winget install
390
+ Microsoft.EdgeWebView2Runtime`) and more than 4 GB RAM without a pagefile.
391
+
392
+ Updates are blocked by policy (a WSUS at 127.0.0.1) and disabled services. To undo:
393
+ delete `HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate` and set `wuauserv`,
394
+ `UsoSvc` and `WaaSMedicSvc` back to manual (Start=3).
395
+
396
+ [odt]: https://learn.microsoft.com/microsoft-365-apps/deploy/overview-office-deployment-tool
397
+
398
+ ## Notes
399
+
400
+ * **The Windows download.** winvm downloads one pinned ISO (`settings.windows`:
401
+ Windows 11 25H2, English) from Microsoft's own CDN and checks its sha256. It does
402
+ not script Microsoft's download page or use third-party mirrors; for other
403
+ languages or versions, download the ISO yourself and pass `--iso`, or pin another
404
+ one in `settings.windows` (url + sha256 + size). An ISO you bring is identified by
405
+ its sha256 too; winvm says whether it matches the one it knows.
406
+ * **pe.cmd wipes disk 0** of the VM it runs in. It refuses to run unless the SMBIOS
407
+ vendor is QEMU, and a slim capture only writes to a disk of exactly the capture size.
408
+ * The debloat lists (what gets removed) ship inside the package. To change them,
409
+ edit `src/winvm/data/guest/lists/*.txt` in a checkout and install it with
410
+ `uv tool install -e .` (editable); images rebuild because their hash changes.