Put the developer docs under docs/dev and index the folder for users first
docs/ had 26 developer documents flat beside the manual, and the two audiences are very differently sized: most readers want the manual and the gesture reference, a few want the register, the designs and the measurements. The manual and gestures.md stay at the top; everything for someone changing the code moves to docs/dev/, and the two documents that name their own successors — the v0.1 milestone and the UI-refinement plan — go to docs/dev/archive/ rather than being deleted, since both are still cited. docs/README.md is the index, users first. Every reference follows: code comments, Cargo manifests, the workflows, the pre-commit hook, the bench and traceability tools (which locate the repo root by docs/dev/requirements.md now), packaging, the Docker READMEs, CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level deeper and is regenerated. Links out of the moved documents into the tree gain a level; a link checker over every Markdown file finds none broken.
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
# DarkRoom — A Windows installer from the Linux CI
|
||||
|
||||
**Satisfies:** FR-PLAT-WIN-1 · FR-PLAT-WIN-2 · FR-PLAT-WIN-3 · NFR-COMPAT-2 (a stated channel)
|
||||
**Companion to:** [distribution.md](distribution.md) · [requirements.md](requirements.md) §3.8, §4.4 ·
|
||||
[android-signing.md](android-signing.md)
|
||||
|
||||
Spec for producing `DarkRoom-<version>-x86_64-setup.exe` from the same Gitea runner that builds the
|
||||
Arch package and the APK, with no Windows machine in the loop. It names the toolchain, what the
|
||||
tree has to change to compile for the target, what the installer does, how the CI job is shaped,
|
||||
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.** 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, [`.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.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this is nearly free, and where it is not
|
||||
|
||||
The reason to write this at all is that the tree is closer to Windows than a Linux-only project
|
||||
usually is. The three things that ordinarily make a cross-build to Windows a week of work are all
|
||||
absent:
|
||||
|
||||
| Usual obstacle | Here |
|
||||
|---|---|
|
||||
| A C image library (libraw, libjpeg-turbo, lcms) | `rawler`, `zune-jpeg`, `jpeg-encoder`, all pure Rust |
|
||||
| OpenSSL, or a TLS stack with a system dependency | `reqwest` on `rustls` + `webpki-roots`; `ring` cross-compiles to the GNU target |
|
||||
| A GUI toolkit with a platform-specific build | Slint on `winit` + wgpu, which already runs the same code on Linux and Android |
|
||||
|
||||
`rusqlite` is `bundled`, so SQLite compiles with whatever C compiler the target has; that is the one
|
||||
place a cross C compiler is required, and it is a package install rather than a port. The inference
|
||||
engine is tract, in Rust, which is what [faces.md §3](faces.md) chose it for — and this is the
|
||||
second time that choice pays: the C++ ONNX Runtime would have needed a prebuilt Windows binary
|
||||
fetched at build time.
|
||||
|
||||
**Where it is not free** is `dr-plat` and the handful of paths above it, which is exactly where
|
||||
NFR-PORT-1 says platform code should be and where §3 finds it. That the list in §3 is short and
|
||||
every item on it is already behind a `cfg` is the measure of whether NFR-PORT-3 ("a third platform
|
||||
requires implementing the platform interfaces only") was met. It was, nearly: the gaps are in
|
||||
things that grew *above* `dr-plat` — a settings file path, an `xdg-open` — rather than in the
|
||||
interfaces themselves.
|
||||
|
||||
---
|
||||
|
||||
## 2. Toolchain: the GNU target, from a container
|
||||
|
||||
Two Rust targets can produce a Windows binary from Linux.
|
||||
|
||||
| Target | Linker | What it needs on the runner | What it costs |
|
||||
|---|---|---|---|
|
||||
| **`x86_64-pc-windows-gnu`** | MinGW-w64 `gcc` | `gcc-mingw-w64-x86-64` (Debian/Ubuntu), `mingw-w64-gcc` (Arch) — one apt/pacman install | Binaries link `libgcc_s` and `libwinpthread` unless told not to; the SEH unwinder is MinGW's rather than MSVC's; DirectX bindings are less exercised (not used — §2.1) |
|
||||
| `x86_64-pc-windows-msvc` | `lld-link` via [`cargo-xwin`](https://github.com/rust-cross/cargo-xwin) | The MSVC CRT and Windows SDK headers, fetched from Microsoft's servers by `xwin` on first use (~1.5 GB, licence-accepted by flag) | A download step in CI that depends on Microsoft keeping those URLs stable, and a licence the runner accepts on the project's behalf |
|
||||
|
||||
**Decision: GNU.** It is a package install, it is what `rustup target add` supports out of the
|
||||
box, and every crate in the dependency graph that carries a C component (`ring`, `libsqlite3-sys`,
|
||||
`zstd-sys` if present) builds against MinGW today. The MSVC route produces a marginally more
|
||||
conventional binary — the same CRT every other Windows application links — and costs a
|
||||
1.5 GB fetch of Microsoft-licensed headers on every cold CI run. That is the wrong trade for a
|
||||
channel whose users are, for now, the author.
|
||||
|
||||
Static-link the MinGW runtime so the installer carries one file rather than three. The
|
||||
configuration lives in the container as environment variables rather than in a `.cargo/config.toml`
|
||||
— that file is untracked here on purpose, and the Android image sets its linkers the same way:
|
||||
|
||||
```sh
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc-posix
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-args=-static-libgcc -C link-args=-static-libstdc++"
|
||||
```
|
||||
|
||||
**Corrected.** The first draft added `-Wl,--whole-archive -lwinpthread` "so nothing imports
|
||||
`libwinpthread-1.dll`". That flag breaks the link: forcing the whole archive drags in unused
|
||||
winpthread objects whose kernel32 and msvcrt references land after those libraries on the link
|
||||
line, and the build dies on a hundred undefined `__imp_` symbols. It was also unnecessary —
|
||||
rustc's windows-gnu target links its own winpthread in self-contained mode, and the built
|
||||
executable imports no MinGW library at all (§10). `-posix` is stated because Debian's bare
|
||||
`x86_64-w64-mingw32-gcc` is an alternatives symlink to either thread model.
|
||||
|
||||
Two more things the container needs that the draft did not name: a **host** `gcc`, because build
|
||||
scripts and proc-macros compile for Linux whatever the target and the very first one fails with
|
||||
"linker `cc` not found" without it; and **Wine 10**, because rustc's std imports
|
||||
`bcryptprimitives.dll` for its random source and Debian bookworm's Wine 8.0 does not have it —
|
||||
the smoke test dies at load with `c0000135` before the first instruction. So the image is
|
||||
`debian:trixie-slim`, which also ships Node 20 natively.
|
||||
|
||||
### 2.1 What the binary reaches at runtime
|
||||
|
||||
Nothing the installer has to carry. wgpu opens **Vulkan** (`dr_gpu::new_shared`, D1 — Vulkan on
|
||||
both targets, and the shared-device path permits nothing else), and on Windows the Vulkan loader
|
||||
`vulkan-1.dll` is installed by every GPU vendor's driver. Slint's femtovg renderer finds system
|
||||
fonts through `fontdb`, so the `fontconfig` the Linux CI job installs has no Windows counterpart.
|
||||
There is no `libxkbcommon`, no display-server library: `winit` speaks Win32 directly.
|
||||
|
||||
**DirectX 12 is deliberately not enabled.** wgpu supports it and on Windows would be the
|
||||
conventional choice, but the develop pipeline's compute shaders are written once for Vulkan
|
||||
(NFR-PORT-2) and validated on two Vulkan drivers already; a third backend is a third set of
|
||||
driver behaviours to characterise (NFR-R1's tolerance argument), and no Windows machine that can
|
||||
run this application lacks a Vulkan ICD. The same reasoning that keeps GL out of `new_shared`
|
||||
keeps DX12 out here. It is one flag away if that turns out to be wrong.
|
||||
|
||||
### 2.2 Where it builds
|
||||
|
||||
The same shape as the Android leg: a job container built from a Dockerfile in the tree and pushed
|
||||
to the Gitea registry, tagged by the tree id of its directory so an unrelated push reuses it
|
||||
([`android-image.yml`](../../.gitea/workflows/android-image.yml) already does this and the comment
|
||||
there explains why).
|
||||
|
||||
```
|
||||
docker/windows/
|
||||
Dockerfile debian:trixie + rustup (1.92.0, target x86_64-pc-windows-gnu) + gcc + gcc-mingw-w64-x86-64 + nsis + wine
|
||||
build.sh run a command in the container; caches registry, target and the Wine prefix
|
||||
package.sh the LFS guard, staging, then makensis
|
||||
```
|
||||
|
||||
`makensis` is a Linux binary; NSIS has always been buildable and runnable on POSIX hosts, and
|
||||
Debian ships it as `nsis`. Wine is in the image for §6, not for the build. `osslsigncode` is not
|
||||
in it until there is a certificate to give it (§5.4).
|
||||
|
||||
---
|
||||
|
||||
## 3. What the tree has to change
|
||||
|
||||
Read from the source, not run. Everything here is `dr-plat` or the thin layer above it — nothing
|
||||
in `core/` is touched, which is NFR-PORT-1 holding.
|
||||
|
||||
### 3.1 Already handled
|
||||
|
||||
- **`volumes.rs`** — card detection reads `/proc/mounts` and `/sys/block` under
|
||||
`cfg(target_os = "linux")` and returns an empty list elsewhere. Windows gets no card detection
|
||||
in this pass; the import flow's path picker still works. (A `GetDriveType`/`DRIVE_REMOVABLE`
|
||||
implementation is a screen of code and a follow-up.)
|
||||
- **`display.rs`** — the X11 and Wayland colour-profile readers are `cfg(all(unix, not(android)))`;
|
||||
the fallback is FR-DSP-8's stated one. Windows ICC profiles via `GetICMProfile` are a follow-up
|
||||
for the same reason.
|
||||
- **`desktop_client.rs`** — the Nextcloud desktop client's Unix socket is `cfg(unix)`. On Windows
|
||||
the client listens on a named pipe (`\\.\pipe\...`); until that is implemented FR-NC-6c's
|
||||
integration is absent and the app behaves as it does on a Linux machine with no client running.
|
||||
- **`secrets.rs`** — has a `PlatformSecretStore` for "any platform without an implementation"
|
||||
that returns `SecretError::Unavailable` on every call. It is loud on purpose, so a Windows build
|
||||
made with no further change *compiles*, starts, and fails at sign-in with a clear message. §3.2
|
||||
is what turns that into a working store.
|
||||
- **`keyring`, `x11rb`, `wayland-*`** are target-scoped dependencies already, so the Linux-only
|
||||
crates are not even compiled.
|
||||
|
||||
### 3.2 Required before the installer is worth shipping
|
||||
|
||||
Ordered by what blocks a first sign-in. **All four are done**; each item says how.
|
||||
|
||||
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.** *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 |
|
||||
|---|---|---|
|
||||
| config (`settings.json`, accounts) | `$XDG_CONFIG_HOME/darkroom` — `dr_sync::config_dir`, `settings_store.rs` | `%APPDATA%\darkroom` |
|
||||
| data (catalog, thumbnails, faces) | `$XDG_DATA_HOME/darkroom` — `library::data_root` | `%LOCALAPPDATA%\darkroom` |
|
||||
| state (crash reports, diagnostics) | `$XDG_STATE_HOME/darkroom` — `state.rs`, `crash.rs` | `%LOCALAPPDATA%\darkroom\state` |
|
||||
|
||||
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.** *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.** *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;
|
||||
the first cross-compile will name any that are not, and the fix is a `cfg` rather than a
|
||||
Windows ACL equivalent — the files in question are the user's own.
|
||||
|
||||
6. **The executable's identity.** *Done.* Windows takes the icon and the version block from a
|
||||
resource compiled into the `.exe`, not from a `.desktop` file.
|
||||
[`apps/darkroom-desktop/build.rs`](../../apps/darkroom-desktop/build.rs) uses `winresource`
|
||||
(which invokes MinGW's `windres` when cross-compiling) to embed
|
||||
[`ui/dr-ui/ui/app-icon.png`](../../ui/dr-ui/ui/app-icon.png) — wrapped into an `.ico` in
|
||||
`OUT_DIR` at build time, since an ICO entry may be a PNG, so no generated binary is committed —
|
||||
plus the version from `CARGO_PKG_VERSION` and the product name. The script returns before
|
||||
touching the crate on every other target, and `winresource` is an unconditional
|
||||
build-dependency because **a `cfg(windows)` on a build-dependency is evaluated against the
|
||||
host**, which is Linux. This is the fifth place the identifier lives, and
|
||||
[`tools/set-version.sh`](../../tools/set-version.sh) does not need to learn it: the resource
|
||||
reads the version cargo already knows. The same commit made the release binary a GUI-subsystem
|
||||
executable (`windows_subsystem = "windows"`), or Windows keeps a console window open behind
|
||||
the application.
|
||||
|
||||
Everything in this list is `cfg(windows)` code in `dr-plat` or a call-site switch in `dr-ui`, and
|
||||
none of it touches the image core, the catalog schema, or the edit pipeline. That is the NFR-PORT-3
|
||||
test, and it should be stated in the commit that closes the list whether it passed.
|
||||
|
||||
**What the first cross-compile actually found** (§9 step 2): nothing in this list blocked the
|
||||
link. The whole graph compiled; the only warnings were two constants — `SERVICE` in `secrets.rs`
|
||||
and `TIMEOUT` in `desktop_client.rs` — left unused by the `cfg`s that already shadow their users,
|
||||
now guarded the same way. Items 1–4 are still open, and the binary starts without them; it just
|
||||
cannot sign in.
|
||||
|
||||
### 3.3 Explicitly not in this pass
|
||||
|
||||
- **MIME/file-type registration** — FR-PLAT-LIN-1's `.desktop` MIME entries have a registry
|
||||
equivalent (`HKCU\Software\Classes\.cr2` etc.). Not until the application opens a file from the
|
||||
command line usefully, which `main.rs` accepts but the launch flow does not yet act on.
|
||||
- **High-DPI declaration** — winit sets per-monitor-v2 awareness through its manifest by default.
|
||||
Verified in winit's source, not on a monitor; if text is blurry on a 150% display this is the
|
||||
first suspect.
|
||||
- **Card detection, ICC profiles, the desktop-client pipe** — §3.1's three follow-ups.
|
||||
- **A GL or DX12 fallback** — §2.1. A machine without Vulkan gets the library and no develop
|
||||
path, which is what it gets on Linux too.
|
||||
|
||||
---
|
||||
|
||||
## 4. The build script
|
||||
|
||||
`docker/windows/build.sh`, in the shape of the Android one and with the same rules:
|
||||
|
||||
```sh
|
||||
cargo build --release --target x86_64-pc-windows-gnu -p darkroom-desktop
|
||||
```
|
||||
|
||||
Release only, with `CARGO_TARGET_DIR` inside the workspace so the CI cache key
|
||||
(`windows-${{ hashFiles('**/Cargo.lock') }}`) covers it. The whole workspace is *not* built for
|
||||
the target: `darkroom-android` cannot be, and the examples that need a display or a catalog on
|
||||
disk have nothing to run against. `cargo clippy --target x86_64-pc-windows-gnu -p darkroom-desktop`
|
||||
is worth running in the same job, because the `cfg(windows)` branches from §3 are otherwise
|
||||
never linted — the Linux job cannot see them.
|
||||
|
||||
Not `cargo test --target x86_64-pc-windows-gnu`: the test binaries would be Windows executables,
|
||||
and running them means Wine. §6 does that for exactly one binary, deliberately.
|
||||
|
||||
---
|
||||
|
||||
## 5. The installer
|
||||
|
||||
[`packaging/windows/darkroom.nsi`](../../packaging/windows/darkroom.nsi), compiled by `makensis` on
|
||||
the runner into `DarkRoom-<version>-x86_64-setup.exe`. `package.sh` passes the version in
|
||||
(`/DVERSION=…`, from `tools/set-version.sh`'s single source, the workspace `Cargo.toml`) and refuses
|
||||
to run if any `models/face/*.onnx` is smaller than 100 KB — the LFS-pointer guard every other
|
||||
packager carries, for the reason [distribution.md §1](distribution.md) gives.
|
||||
|
||||
### 5.1 Per-user, not per-machine
|
||||
|
||||
Install to `$LOCALAPPDATA\Programs\DarkRoom`, register the uninstaller under
|
||||
`HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\DarkRoom`, `RequestExecutionLevel user`.
|
||||
No UAC prompt, no `Program Files`, no writes outside the user's profile. This is the shape VS Code's
|
||||
"User Installer" and most Electron applications use, and it is right for this project for two
|
||||
reasons: an unsigned installer that also asks for administrator rights is the most alarming thing
|
||||
Windows can show a user (§5.4), and a per-user install means the application's own data directories
|
||||
(§3.2) and its binaries are governed by the same account, which is what NFR-SEC-5's
|
||||
"the user's own hardware" means on a shared machine.
|
||||
|
||||
### 5.2 What it puts on disk
|
||||
|
||||
```
|
||||
$LOCALAPPDATA\Programs\DarkRoom\
|
||||
darkroom.exe
|
||||
models\
|
||||
scrfd_500m_640.onnx scrfd_2.5g_640.onnx scrfd_10g_640.onnx arcface_mbf_b1.onnx
|
||||
2d106det_b1.onnx ocec_s_b1.onnx sgc_l_48_b1.onnx
|
||||
yolo26s-sem-ade20k.onnx yolo26s-sem-ade20k.classes.json categories.txt
|
||||
LICENSE
|
||||
uninstall.exe
|
||||
```
|
||||
|
||||
Plus a Start Menu shortcut, and nothing on the desktop unless the user ticks it. The models are
|
||||
the same ten files the APK bundles and the PKGBUILD installs; `models\` beside the executable is
|
||||
where §3.2's lookup finds them. **No `LICENSE` yet**: the repository has no licence file at its
|
||||
root (the Arch package points at the system's shared GPL text), so the installer has no licence
|
||||
page until one is added — a one-file change, and the `.nsi` says where the page then goes. The face weights carry the research-only grant that
|
||||
[faces.md §2](faces.md) records, and this channel changes nothing about that: the installer is
|
||||
for the author's own machines until §2.2a's caveat is resolved, exactly as the APK is.
|
||||
|
||||
### 5.3 Uninstall
|
||||
|
||||
Removes the install directory, the shortcut and the registry key. **Does not touch
|
||||
`%LOCALAPPDATA%\darkroom` or `%APPDATA%\darkroom`** — the catalog, the thumbnails, the face
|
||||
index, the settings. An uninstaller that deletes a library index the user spent two hours building
|
||||
is the kind of destructive default FR-CULL-12 and NFR-SEC-5's "disabling deletes nothing" both
|
||||
argue against. The uninstaller says so on its one page, and names the two directories so a user who
|
||||
does want them gone knows where they are.
|
||||
|
||||
### 5.4 Signing, and the warning that results from not doing it
|
||||
|
||||
An unsigned installer triggers SmartScreen's "Windows protected your PC" interstitial, dismissable
|
||||
through "More info → Run anyway". Signing needs an Authenticode certificate, which is paid and
|
||||
identity-verified; from Linux the signing itself is `osslsigncode`, which is why it is in the
|
||||
container image, but there is no certificate to give it. **This spec ships unsigned** and the
|
||||
release notes say what the interstitial looks like. An OV certificate is a cost decision to make
|
||||
if this channel ever has a user who is not the author; an EV one buys instant reputation and costs
|
||||
a hardware token. Neither is a build problem.
|
||||
|
||||
The APK went through the same sequence — [android-signing.md](android-signing.md) records a
|
||||
debug-signed build becoming a release-signed one when it mattered — and this channel should be
|
||||
allowed to do the same.
|
||||
|
||||
### 5.5 What NSIS is chosen over
|
||||
|
||||
WiX produces an MSI, which is what enterprise deployment tooling wants and what nobody deploying
|
||||
a photo editor to their own laptop cares about; its Linux story is `wixl` from msitools, which is
|
||||
real but thinly used. Inno Setup runs only under Wine. NSIS is scriptable in plain text, builds
|
||||
natively on Linux, produces a single self-contained `.exe`, and the script for §5.2 is under a
|
||||
hundred lines. It is the conventional answer for exactly this situation.
|
||||
|
||||
One choice inside NSIS: `Target amd64-unicode`, a 64-bit installer rather than the default 32-bit
|
||||
stub. The application is x86_64 only so nothing is lost, and it is what lets §6's install test run
|
||||
under a 64-bit-only Wine — the 32-bit stub needs an i386 multiarch Wine and dies loading the WoW64
|
||||
`ntdll` without one.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verifying without Windows
|
||||
|
||||
This is the part to be honest about. The runner has no Windows, no GPU it can hand to a Windows
|
||||
process, and no display. What *can* be checked, in increasing cost and decreasing certainty:
|
||||
|
||||
| Check | How | What it proves |
|
||||
|---|---|---|
|
||||
| **It links** | the build succeeds | Every `cfg(windows)` branch compiles; no `unix`-only symbol leaked past a `cfg` |
|
||||
| **It is a Windows executable** | `file darkroom.exe` reports PE32+; `x86_64-w64-mingw32-objdump -p` lists the DLLs it imports and none are MinGW's | The static-runtime flags in §2 held |
|
||||
| **It starts** | `wine64 darkroom.exe --version` exits 0 and prints the version | The CRT, the resource block and `main` are sound; paths in §3.2 resolve (Wine sets `LOCALAPPDATA`) |
|
||||
| **The installer runs** | `wine64 DarkRoom-setup.exe /S` then the install directory exists with the eight files, and `wine64 uninstall.exe /S` removes it | The NSIS script's file list, sections and uninstaller are right |
|
||||
| **It draws a window** | `xvfb-run wine64 darkroom.exe` with `SLINT_WGPU_CPU` and a lavapipe ICD exposed through `winevulkan` | That Slint's winit backend initialises on Win32 — and this is where the chain gets long enough that a failure says more about Wine than about DarkRoom |
|
||||
|
||||
The first four are the CI gate. The fifth is worth trying once by hand and not putting in CI:
|
||||
it needs `winevulkan` to find a host ICD, `xvfb`, and a Wine prefix warmed up in the container,
|
||||
and every one of those is a moving part that has nothing to do with whether the application works
|
||||
on Windows.
|
||||
|
||||
`--version` exists for this — a smoke test needs an exit that opens no window and touches no
|
||||
directory — and it is answered before the logger and the crash hook install, so it proves the CRT
|
||||
and the resource block and nothing above them.
|
||||
|
||||
**One more row the table missed:** the Start Menu shortcut. `CreateShortcut` is `IShellLink`,
|
||||
which does nothing under a headless Wine while the `CreateDirectory` beside it succeeds, so an
|
||||
installer that installs and uninstalls cleanly here can still have a broken shortcut. That row is
|
||||
on Windows only.
|
||||
|
||||
**What none of this proves:** that wgpu opens a Vulkan device on a real driver, that a 6000-px
|
||||
render completes, that fonts are found, that the secret store round-trips. Those are a person with
|
||||
a Windows machine, once per release, until there is a Windows runner — and a self-hosted Windows
|
||||
act_runner is how that would be done, not a cloud service. The release notes for the first build
|
||||
say which of these were checked and on what.
|
||||
|
||||
---
|
||||
|
||||
## 7. The CI job
|
||||
|
||||
A fourth leg of [`build-and-test.yml`](../../.gitea/workflows/build-and-test.yml), beside desktop,
|
||||
Android and traceability:
|
||||
|
||||
```yaml
|
||||
windows-image:
|
||||
uses: ./.gitea/workflows/windows-image.yml # same shape as android-image.yml
|
||||
|
||||
windows:
|
||||
runs-on: linux/amd64
|
||||
name: Windows (x86_64, cross)
|
||||
needs: windows-image
|
||||
container:
|
||||
image: gitea.tourolle.paris/dtourolle/darkroom-windows:latest
|
||||
steps:
|
||||
- checkout, LFS pull # copied from the desktop leg
|
||||
- cache: ~/.cargo, target # key: windows-${{ hashFiles('**/Cargo.lock') }}
|
||||
- docker/windows/build.sh # cargo build + clippy, --target x86_64-pc-windows-gnu
|
||||
- smoke: file, objdump, wine64 --version # §6 rows 1–3
|
||||
- docker/windows/package.sh # LFS guard, makensis
|
||||
- smoke: wine64 setup.exe /S; ls; uninstall # §6 row 4
|
||||
- upload artefact: DarkRoom-*-setup.exe # on tags only, like the APK
|
||||
```
|
||||
|
||||
Same gotchas as the Android leg, which its comments already record: the host has no Node, so the
|
||||
checkout is plain `git`; workflow inputs arrive as strings; the image build needs the host Docker
|
||||
daemon and runs outside a container. None of that is new.
|
||||
|
||||
**Cost.** A cold build of the whole graph for a second target is roughly the desktop leg again —
|
||||
tract, Slint's compiler, wgpu — so with the cargo cache warm it is minutes and cold it is the
|
||||
better part of half an hour. Worth noting because the runner is one machine and the legs run in
|
||||
parallel on it; if it starts starving the desktop leg, `needs: desktop` serialises them.
|
||||
|
||||
---
|
||||
|
||||
## 8. Requirements
|
||||
|
||||
Three, added to [requirements.md §3.8](requirements.md) under a `#### Windows` heading beside the
|
||||
Linux ones. Phrased to be testable, and each one is something §3 or §5 would otherwise leave as a
|
||||
convention.
|
||||
|
||||
**FR-PLAT-WIN-1 — Known folders.** Configuration under `%APPDATA%\darkroom`; data, cache and
|
||||
state under `%LOCALAPPDATA%\darkroom`. No file under the user's profile root and nothing relative
|
||||
to the working directory. The directory *layout* beneath those roots is the same as under XDG, so
|
||||
a library directory moves between platforms unchanged.
|
||||
|
||||
**FR-PLAT-WIN-2 — Installer.** A per-user installer that needs no elevation, registers an
|
||||
uninstaller, and whose uninstaller removes what the installer wrote and nothing the application
|
||||
wrote. Models are installed beside the executable and found there last, after the user's own
|
||||
directories.
|
||||
|
||||
**FR-PLAT-WIN-3 — Built from Linux.** The Windows binary and its installer are produced by the
|
||||
Linux CI from the same commit as every other channel, with no Windows machine in the build.
|
||||
Verification on Windows is a release step, recorded per release, not a build step.
|
||||
|
||||
NFR-COMPAT-2's channel table in [distribution.md §1](distribution.md) gains a row. NFR-PORT-3 gets
|
||||
its first real test, and the commit that closes §3.2 records the answer.
|
||||
|
||||
---
|
||||
|
||||
## 9. Order
|
||||
|
||||
1. `--version` in `main.rs`, and the `.cargo/config.toml` target block. Trivial, and the smoke
|
||||
test in §6 needs both before anything else can be measured.
|
||||
2. `rustup target add x86_64-pc-windows-gnu`, `pacman -S mingw-w64-gcc`, and a first
|
||||
`cargo build --target …` on the developer machine — **before the container exists**, because
|
||||
the list in §3.2 is a reading of the source and the compiler's list will be longer. Fix the
|
||||
`cfg` fallout as it appears. This is the afternoon that decides whether §1's optimism holds.
|
||||
3. §3.2 items 1–4, each its own commit, each stating which NFR-PORT interface it implemented.
|
||||
4. §3.2 item 6 — the resource block — and the NSIS script; `makensis` by hand; `wine64 setup.exe /S`
|
||||
by hand. Now there is an artefact.
|
||||
5. The container, the image workflow, the CI leg. Only after 4 works locally, for the same reason
|
||||
the Android image was reproduced from the tree after it had lived on one laptop.
|
||||
6. A build on a real Windows machine, and a note in the release saying what was checked.
|
||||
|
||||
Steps 1–2 are cheap and either confirm this document or replace §3.2 with the true list. Nothing
|
||||
past step 2 should be started on the strength of this document alone.
|
||||
|
||||
---
|
||||
|
||||
## 10. Report · 2026-09-12
|
||||
|
||||
Steps 1, 2 and 4 run, in the container rather than on the developer machine, because the
|
||||
container was the cheaper way to get a pinned MinGW and a Wine that could be thrown away.
|
||||
|
||||
| §6 row | Result |
|
||||
|---|---|
|
||||
| It links | Yes, first attempt once the link flags were right. 115 MB, `PE32+ … (GUI)`. Two dead-code warnings, both `cfg`-shadowed constants, fixed. |
|
||||
| It is a Windows executable | 26 imports, all Windows system DLLs. No MinGW runtime. `.rsrc` carries `PRODUCTVERSION 0,12,0,0`, `ProductName DarkRoom`, the icon. |
|
||||
| It starts | `wine darkroom-desktop.exe --version` → `darkroom-desktop 0.12.0`, exit 0, 0.1 s. |
|
||||
| The installer runs | `makensis` → 105 MB. `/S` installs the exe and ten models to `AppData\Local\Programs\DarkRoom`, writes the `HKCU` uninstall key; the installed exe runs; `uninstall.exe /S` removes directory and key. Shortcut unverifiable (§6). |
|
||||
| It draws a window | Not attempted. |
|
||||
|
||||
**What the first draft got wrong**, kept in place above with a note rather than rewritten, because
|
||||
the reasoning that produced each mistake is the thing a reader will otherwise repeat:
|
||||
|
||||
1. The `--whole-archive -lwinpthread` link flag (§2) — breaks the link and was never needed.
|
||||
2. No host C compiler in the image (§2) — build scripts are host binaries.
|
||||
3. Debian bookworm's Wine (§2) — lacks `bcryptprimitives.dll`, which rustc's std imports.
|
||||
4. A `cfg(windows)` on the `winresource` build-dependency (§3.2 item 6) — evaluated against the
|
||||
host, so the crate was silently absent from the cross-build.
|
||||
|
||||
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).
|
||||
|
||||
**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.
|
||||
Reference in New Issue
Block a user