From b718c70b110cba56008063cf290c8895f231c70c Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Fri, 11 Sep 2026 23:30:16 +0200 Subject: [PATCH] Specify a Windows installer built by the Linux CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tree is closer to Windows than a Linux-only project usually is: every image library, the TLS stack and the inference engine are pure Rust, and dr-plat already keeps the Linux-only code behind cfgs with a loud fallback where none exists for another platform. What remains is a short list above dr-plat — five XDG path lookups, an xdg-open, the secret store's third implementation, the models' lookup beside the executable — and none of it touches core, which is the NFR-PORT-3 test this would be the first real run of. docs/windows.md decides the GNU target over MSVC-via-xwin, Vulkan only as on every other platform, a per-user NSIS installer that leaves the library alone on uninstall, and a CI leg in the shape of the Android one. It is explicit about what a runner with no Windows can verify — that it links, is PE32+, starts under Wine and installs under Wine — and what it cannot, which is everything involving a real GPU driver. Three FR-PLAT-WIN requirements and a channel row record the decisions; the ordering puts a first cross-compile on the developer machine before any container exists, because the list of cfg gaps is a reading of the source and the compiler's list will be longer. --- docs/distribution.md | 5 +- docs/requirements.md | 18 ++ docs/traceability.md | 11 +- docs/windows.md | 398 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 427 insertions(+), 5 deletions(-) create mode 100644 docs/windows.md diff --git a/docs/distribution.md b/docs/distribution.md index 90e3075..9a7e4c0 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -22,8 +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 | -Three of these five exist as recipes and two do not. That is stated rather than +Three of these six exist as recipes and three 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. @@ -245,6 +246,8 @@ packaging/ paris.tourolle.darkroom.metainfo.xml AppStream, installed by every channel flatpak/ paris.tourolle.darkroom.yml the manifest, and where the permissions are argued + windows/ + darkroom.nsi specified in windows.md §5; not yet written ``` `packaging/` also accumulates built `.pkg.tar.zst` artefacts from local diff --git a/docs/requirements.md b/docs/requirements.md index a489311..ec8c95d 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -1009,6 +1009,24 @@ protocol is unavailable, FR-DSP-8's stated fallback applies. portals and credential storage uses the Secret Service portal, both verified to satisfy FR-NC-2 and FR-CAT-1 within the sandbox. +#### Windows + +Specified in [windows.md](windows.md); a stated channel under NFR-COMPAT-2, not a v1 one. + +**FR-PLAT-WIN-1 — Known folders.** Configuration under `%APPDATA%\darkroom`; data, cache and +state under `%LOCALAPPDATA%\darkroom`. Nothing under the profile root and nothing relative to the +working directory. The 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. + ### 3.9 Culling Per D11 this is the product's primary differentiator, not an incidental capability. diff --git a/docs/traceability.md b/docs/traceability.md index 5ca9843..4e0d706 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -11,15 +11,15 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n |---|---| | Source files scanned | 350 | | TRACES tags found | 1377 | -| Requirements defined | 187 | +| Requirements defined | 190 | | Requirements covered | 134 | -| **Coverage** | **71.7%** (134/187) | +| **Coverage** | **70.5%** (134/190) | ### By type | Type | Covered | Defined | |---|---|---| -| FR | 101 | 132 | +| FR | 101 | 135 | | NFR | 30 | 49 | | R | 3 | 6 | @@ -170,7 +170,7 @@ _None._ ## Not yet tagged -53 of 187 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. +56 of 190 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
Show untagged requirements @@ -184,6 +184,9 @@ _None._ - FR-NC-11 - FR-PLAT-AND-1 - FR-PLAT-LIN-3 +- FR-PLAT-WIN-1 +- FR-PLAT-WIN-2 +- FR-PLAT-WIN-3 - FR-PLG-1 - FR-PLG-10 - FR-PLG-11 diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..1deae56 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,398 @@ +# 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--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. + +**It is a spec, not a report.** Nothing below has been built. Where a claim rests on reading rather +than running, it says so, in the same discipline [faces.md §2.3](faces.md) applies to licences: the +cheap way to find out is to read first, and the expensive way is at packaging time. + +--- + +## 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: + +```toml +# .cargo/config.toml +[target.x86_64-pc-windows-gnu] +linker = "x86_64-w64-mingw32-gcc" +ar = "x86_64-w64-mingw32-gcc-ar" +rustflags = ["-C", "link-args=-static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive"] +``` + +The `winpthread` incantation is the well-known one and is stated rather than derived; it is the +first thing to check if the resulting `.exe` complains about a missing `libwinpthread-1.dll`. + +### 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 ubuntu + rustup (1.92.0, target x86_64-pc-windows-gnu) + gcc-mingw-w64-x86-64 + nsis + wine64 + osslsigncode + build.sh cargo build --release --target x86_64-pc-windows-gnu -p darkroom-desktop + package.sh the LFS guard, 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. + +--- + +## 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. + +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. + +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: + + | 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.** `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. + +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 "" `; 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. + +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.** Windows takes the icon and the version block from a resource + compiled into the `.exe`, not from a `.desktop` file. A `build.rs` in `darkroom-desktop` using + `winresource` (which invokes MinGW's `windres` when cross-compiling) embeds + [`ui/dr-ui/ui/app-icon.png`](../ui/dr-ui/ui/app-icon.png) converted to `.ico`, the version + from `CARGO_PKG_VERSION`, and the product name. This is the fourth place the identifier lives + (distribution.md §1's "one identifier, four places" becomes five), and + [`tools/set-version.sh`](../tools/set-version.sh) does not need to learn it: the resource reads + the version cargo already knows. + +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. + +### 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--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 + 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 seven files the APK bundles and the PKGBUILD installs; `models\` beside the executable is +where §3.2's lookup finds them. 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. + +--- + +## 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` does not exist yet — `main.rs` takes paths and nothing else — and it is added for this, +because a smoke test needs an exit that does not open a window. It is two lines. + +**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.