Build the Windows installer in CI

The fourth leg of build-and-test.yml, in the shape of the Android one:
an image workflow that builds docker/windows and pushes it tagged by
the directory's tree id, and a job inside that image that lints the
Windows target — the only place the cfg(windows) branches are ever
compiled by CI — builds, runs the smoke tests docs/windows.md §6
specifies, packages, installs and uninstalls under Wine, and uploads
the installer. Every step was run by hand in the same container first.

The spec's open list closes with this: the four §3.2 items, the
licence page, and the leg. What remains is what Wine cannot show, and
§10 now lists it as the first real Windows run's checklist.
This commit is contained in:
2026-09-12 07:34:10 +02:00
parent 6609aa9acf
commit 43f70c4765
5 changed files with 336 additions and 37 deletions
+3 -3
View File
@@ -22,9 +22,9 @@ permission to a package rather than after.
| Linux | AppImage | v1 channel, **recipe not yet written** (§5) | Oldest supported glibc, and no sandbox at all |
| Android | F-Droid | v1 channel, not yet submitted | GPLv3-clean build, reproducible, no proprietary blobs |
| Android | Play Store | **Not v1** (§6) | Would make ARCH §6.9 binding as policy rather than as engineering |
| Windows | NSIS per-user installer, cross-built — [windows.md](windows.md) | **Specified, not built.** Not v1 | Known folders in place of XDG; no sandbox; unsigned until there is a certificate |
| Windows | NSIS per-user installer, cross-built — [windows.md](windows.md) | Built by CI, **untested on Windows**. Not v1 | Known folders in place of XDG; no sandbox; unsigned until there is a certificate |
Three of these six exist as recipes and three do not. That is stated rather than
Four of these six exist as recipes and two do not. That is stated rather than
smoothed over, because the value of writing the channels down is knowing which
constraints are already being met and which are promises.
@@ -247,7 +247,7 @@ packaging/
flatpak/
paris.tourolle.darkroom.yml the manifest, and where the permissions are argued
windows/
darkroom.nsi specified in windows.md §5; not yet written
darkroom.nsi the installer; docker/windows/package.sh drives it
```
`packaging/` also accumulates built `.pkg.tar.zst` artefacts from local
+46 -28
View File
@@ -10,11 +10,12 @@ tree has to change to compile for the target, what the installer does, how the C
and — because there is no Windows hardware on the runner — exactly how much of the result can be
verified before a person double-clicks it.
**Written as a spec; §10 is the report.** §9's steps 1, 2 and 4 have since been run —
**Written as a spec; §10 is the report.** Every step of §9 has since been run —
[`docker/windows/`](../docker/windows/) is the container, [`packaging/windows/darkroom.nsi`](../packaging/windows/darkroom.nsi)
the installer, and §6's gate passes through row 4 under Wine. Four claims in the first draft were
wrong and are corrected in place with a note; §10 lists them. Where a claim still rests on reading
rather than running, it says so.
the installer, [`.gitea/workflows/windows-image.yml`](../.gitea/workflows/windows-image.yml) and
the `windows` job in `build-and-test.yml` the CI leg, and §6's gate passes through row 4 under
Wine. Four claims in the first draft were wrong and are corrected in place with a note; §10 lists
them. Where a claim still rests on reading rather than running, it says so.
---
@@ -146,21 +147,27 @@ in `core/` is touched, which is NFR-PORT-1 holding.
### 3.2 Required before the installer is worth shipping
Ordered by what blocks a first sign-in.
Ordered by what blocks a first sign-in. **All four are done**; each item says how.
1. **Secret store.** `keyring` has a Windows Credential Manager backend; enable its feature under a
`cfg(windows)` target dependency and add the third `impl SecretStore for PlatformSecretStore`
beside the two that exist. The feature's exact name has changed across `keyring` majors
(`windows-native` in 3.x) and is to be read from the 4.x changelog rather than assumed. FR-NC-2's
rule carries over unchanged: Credential Manager is always present on Windows, so there is no
degraded mode to state.
1. **Secret store.** *Done.* `keyring` 4's `v1` feature set — the one the workspace already
asks for — includes `windows-native-keyring-store`, so the Credential Manager backend needed
no new feature name, only the crate as a `cfg(windows)` target dependency and the existing
Secret Service implementation's `cfg` widened to include Windows. One implementation over
both, because `keyring::Entry` is the same API over either; the only difference is that
`is_available`'s probe always succeeds on Windows, which is correct — Credential Manager is
always present, so FR-NC-2's degraded mode does not arise.
2. **Paths.** FR-PLAT-LIN-1 says XDG, and the code says it in five places by reading `XDG_*_HOME`
and falling back to `$HOME/.local/...`. On Windows `HOME` is normally unset, so every one of
these degrades to a relative path from the working directory — which for a Start Menu launch is
`C:\Windows\System32`. The fix is one function per kind of directory in `dr-plat`, with the
Windows branch reading `%APPDATA%` (config; roams) and `%LOCALAPPDATA%` (data, cache, state;
does not), and the five call sites using it:
2. **Paths.** *Done* — [`platform/dr-plat/src/dirs.rs`](../platform/dr-plat/src/dirs.rs). FR-PLAT-LIN-1
says XDG, and the code said it in five places by reading `XDG_*_HOME` and falling back to
`$HOME/.local/...`. On Windows `HOME` is normally unset, so every one of these degraded to a
relative path from the working directory — which for a Start Menu launch is
`C:\Windows\System32`. Now one function per kind of directory in `dr-plat`, with the Windows
branch reading `%APPDATA%` (config; roams) and `%LOCALAPPDATA%` (data, cache, state; does
not), and the five call sites using it. The Android overrides (`set_state_dir`,
`set_data_dir`) stay where they were; only the fallback behind them moved. Both platforms'
rules are unit-tested on either host, and the Windows one was confirmed by running the
application under Wine: the log landed in `AppData\Local\darkroom\state` and nothing was
written anywhere else.
| Kind | Linux today | Windows |
|---|---|---|
@@ -171,15 +178,18 @@ Ordered by what blocks a first sign-in.
FR-PLAT-WIN-1 states this as the requirement. The catalog and thumbnail *formats* do not change,
so a library directory copied from a Linux machine opens.
3. **Face models.** `library::system_face_models_dirs` walks `$XDG_DATA_DIRS`, which does not
exist on Windows. The installer puts the models beside the executable (§5), so the Windows
branch returns `current_exe().parent().join("models")`. The user-directory lookups above it are
unchanged, so a hand-placed pair still outranks the installed one, exactly as on Linux.
3. **Face models.** *Done.* `library::system_face_models_dirs` walked `$XDG_DATA_DIRS`, which
does not exist on Windows. The rule moved to `dr_plat::system_data_dirs`: the installer puts
the models beside the executable (§5), so the Windows branch returns the executable's own
directory. The user-directory lookups above it are unchanged, so a hand-placed pair still
outranks the installed one, exactly as on Linux.
4. **Opening the sign-in URL.** `launch_ui.rs` shells out to `xdg-open`. Windows wants
`ShellExecuteW` with the `open` verb, or `cmd /C start "" <url>`; the `open` crate does the
`cfg` for every platform and is the conventional answer. Android has its own Intent path
already, so this is the third branch of a function that already has two.
4. **Opening the sign-in URL.** *Done.* `launch_ui.rs` shelled out to `xdg-open`. The Windows
branch runs `rundll32 url.dll,FileProtocolHandler <url>`, which is `ShellExecute` on the URL
and needs no crate — chosen over `cmd /C start`, whose quoting of `&` in a query string is a
known trap, and over the `open` crate, which would be a dependency for one line. Android has
its own Intent path already, so this is the third branch of a function that already had two.
*Not verified*: Wine has no browser to open.
5. **`std::os::unix` uses outside a `cfg`.** `diagnostics.rs` and `presets.rs` use
`PermissionsExt` for mode bits on written files. Most are inside `#[cfg(unix)]` blocks already;
@@ -463,6 +473,14 @@ the reasoning that produced each mistake is the thing a reader will otherwise re
And two things it did not know to say: NSIS's default stub is 32-bit (§5.5), and `CreateShortcut`
cannot be verified headless (§6).
**Still open**, in the order §3.2 gives them: the secret store, the known-folder paths, the
models lookup beside the executable, the sign-in URL. The binary that exists today starts, and
cannot sign in. Then §7's CI leg, whose every step has now been run by hand once.
**Closed since**, same day: all four §3.2 items (each says how), a `LICENSE` at the repository
root so the installer has its licence page, and §7's CI leg — `windows-image.yml` and the
`windows` job, every step of which was run by hand in the same container first. The Windows
target is also linted now, with `cargo clippy --target x86_64-pc-windows-gnu -- -D warnings` in
that job, which is the only place the `cfg(windows)` branches are ever compiled by CI.
**What the first real Windows run has to check**, in order, because Wine cannot: that a Vulkan
device opens on a real driver and a render completes; that fonts are found; that Credential
Manager round-trips a sign-in and the browser opens for Login Flow v2; that the Start Menu
shortcut exists; and that text is sharp on a scaled display (§3.3). The release notes for the
first build should say which of these were checked and on what machine.