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.
399 lines
23 KiB
Markdown
399 lines
23 KiB
Markdown
# 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.
|
||
|
||
**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 "" <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.
|
||
|
||
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-<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
|
||
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.
|