The first CI run of the Windows leg passed every step up to packaging and died in makensis with LicenseData: open failed "target-windows/installer/stage\LICENSE". CI sets CARGO_TARGET_DIR to the relative target-windows, and NSIS on a POSIX host translates the backslash in a File path only when a leading / tells it the path is a POSIX one; a relative name reaches it with the backslash intact. Locally the target was always /work/…, which is why it never showed. package.sh now resolves its directories with realpath first. It also removes any installer already in the output directory before writing the new one. That directory is cached between runs, so after a version bump a glob over it finds two, and the smoke test hands Wine both names joined by a newline as one path — which is what happened locally the moment the version moved to 0.12.1.
Windows cross-build environment
Reproducible container for building the Windows executable and its installer from Linux. The specification is docs/windows.md; this directory is what it turned into, and every departure from the spec's first draft is recorded in the Dockerfile's comments.
Use
# Cross-compile the desktop application
./docker/windows/build.sh cargo build --release --target x86_64-pc-windows-gnu -p darkroom-desktop
# Lint the cfg(windows) branches, which the Linux job never sees
./docker/windows/build.sh cargo clippy --target x86_64-pc-windows-gnu -p darkroom-desktop -- -D warnings
# Build the installer from that binary
./docker/windows/build.sh docker/windows/package.sh
# Smoke-test under Wine (docs/windows.md §6)
./docker/windows/build.sh wine target-windows/x86_64-pc-windows-gnu/release/darkroom-desktop.exe --version
./docker/windows/build.sh wine target-windows/installer/DarkRoom-0.12.0-x86_64-setup.exe /S
# Interactive shell
./docker/windows/build.sh
# After editing the Dockerfile
./docker/windows/build.sh --rebuild
Prefers podman, falls back to docker. The cargo registry, the target directory and the Wine
prefix persist under ~/.cache/darkroom-windows/, so a warm rebuild is minutes and the first
wineboot happens once.
What is verified here, and what is not
Measured on the first build, 2026-09-12:
| Check | Result |
|---|---|
cargo build --target x86_64-pc-windows-gnu -p darkroom-desktop |
Links. 115 MB, PE32+ … (GUI) |
| DLL imports | 26 Windows system DLLs; no libwinpthread-1.dll, libgcc_s, libstdc++ |
wine darkroom-desktop.exe --version |
darkroom-desktop 0.12.0, exit 0, 0.1 s |
makensis |
105 MB DarkRoom-<version>-x86_64-setup.exe, 64-bit stub |
wine setup.exe /S |
Installs exe + 7 models to AppData\Local\Programs\DarkRoom, writes the HKCU uninstall key; the installed exe runs |
wine uninstall.exe /S |
Removes the directory and the key |
| Start Menu shortcut | Not verifiable here. CreateShortcut is IShellLink and does nothing under a headless Wine; the directory beside it is created. Check on Windows. |
Also checked by running the application itself under Wine: its log lands in
AppData\Local\darkroom\state, and nothing is written outside AppData (FR-PLAT-WIN-1).
None of this proves a Vulkan device opens, a render completes, fonts are found, the secret store round-trips or the browser opens for sign-in. Those are a Windows machine, once per release.
In CI
.gitea/workflows/windows-image.yml builds this image and pushes it to
gitea.tourolle.paris/dtourolle/darkroom-windows, exactly as the Android image is handled — tagged
by the tree hash of docker/windows/, rebuilt when and only when a file here changes. The
windows job in build-and-test.yml then runs, inside it, the same commands listed above plus
cargo clippy --target x86_64-pc-windows-gnu -- -D warnings, and uploads the installer as an
artefact.