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.
- winvm-1.2.0/LICENSE +21 -0
- winvm-1.2.0/PKG-INFO +410 -0
- winvm-1.2.0/README.md +394 -0
- winvm-1.2.0/pyproject.toml +28 -0
- winvm-1.2.0/setup.cfg +4 -0
- winvm-1.2.0/src/winvm/__init__.py +2 -0
- winvm-1.2.0/src/winvm/__main__.py +5 -0
- winvm-1.2.0/src/winvm/balloon.py +132 -0
- winvm-1.2.0/src/winvm/build.py +406 -0
- winvm-1.2.0/src/winvm/cli.py +813 -0
- winvm-1.2.0/src/winvm/config.py +387 -0
- winvm-1.2.0/src/winvm/data/guest/autounattend.xml +17 -0
- winvm-1.2.0/src/winvm/data/guest/lists/appx-keep.txt +10 -0
- winvm-1.2.0/src/winvm/data/guest/lists/capabilities-remove.txt +23 -0
- winvm-1.2.0/src/winvm/data/guest/lists/features-keep-payload.txt +7 -0
- winvm-1.2.0/src/winvm/data/guest/lists/features-remove.txt +10 -0
- winvm-1.2.0/src/winvm/data/guest/lists/offline-delete.txt +10 -0
- winvm-1.2.0/src/winvm/data/guest/lists/offline-reg.txt +136 -0
- winvm-1.2.0/src/winvm/data/guest/lists/services-disable.txt +51 -0
- winvm-1.2.0/src/winvm/data/guest/payload/SetupComplete.cmd +4 -0
- winvm-1.2.0/src/winvm/data/guest/payload/common.ps1 +16 -0
- winvm-1.2.0/src/winvm/data/guest/payload/final.ps1 +16 -0
- winvm-1.2.0/src/winvm/data/guest/payload/firstlogon.ps1 +60 -0
- winvm-1.2.0/src/winvm/data/guest/payload/setupcomplete.ps1 +117 -0
- winvm-1.2.0/src/winvm/data/guest/pe.cmd +342 -0
- winvm-1.2.0/src/winvm/data/guest/start.cmd +20 -0
- winvm-1.2.0/src/winvm/data/guest/unattend.xml.in +65 -0
- winvm-1.2.0/src/winvm/data/images/base.yaml +37 -0
- winvm-1.2.0/src/winvm/data/winvm.yaml +56 -0
- winvm-1.2.0/src/winvm/guest.py +107 -0
- winvm-1.2.0/src/winvm/host.py +328 -0
- winvm-1.2.0/src/winvm/media.py +129 -0
- winvm-1.2.0/src/winvm/store.py +112 -0
- winvm-1.2.0/src/winvm/util.py +184 -0
- winvm-1.2.0/src/winvm/vm.py +583 -0
- winvm-1.2.0/src/winvm.egg-info/PKG-INFO +410 -0
- winvm-1.2.0/src/winvm.egg-info/SOURCES.txt +39 -0
- winvm-1.2.0/src/winvm.egg-info/dependency_links.txt +1 -0
- winvm-1.2.0/src/winvm.egg-info/entry_points.txt +2 -0
- winvm-1.2.0/src/winvm.egg-info/requires.txt +2 -0
- 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.
|