Build the Windows installer in a container, and run it under Wine
docs/windows.md specified it; this is §9 steps 1, 2 and 4 run, and the report in §10. A Debian trixie image with rustup, the MinGW cross compiler, NSIS and Wine; a build.sh in the shape of the Android one; a package.sh that stages the executable and the seven models behind the same LFS-pointer guard every other packager carries, then runs makensis; and the .nsi itself — per-user, no elevation, an uninstaller that leaves the library alone. Measured: the executable links first time once the link flags were right, imports only Windows system DLLs, prints its version under Wine, and the installer installs and uninstalls silently under Wine with the registry key and the models where §5.2 says. What Wine cannot show is the Start Menu shortcut: CreateShortcut is IShellLink and does nothing headless. Four claims in the spec's first draft were wrong and are corrected in place with the reasoning kept: the whole-archive winpthread flag breaks the link and was never needed; build scripts need a host gcc; bookworm's Wine lacks the bcryptprimitives.dll rustc's std imports, so the image is trixie; and NSIS's default stub is 32-bit, so the installer says amd64-unicode and needs no i386 Wine.
This commit is contained in:
+97
-27
@@ -10,9 +10,11 @@ tree has to change to compile for the target, what the installer does, how the C
|
||||
and — because there is no Windows hardware on the runner — exactly how much of the result can be
|
||||
verified before a person double-clicks it.
|
||||
|
||||
**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.
|
||||
**Written as a spec; §10 is the report.** §9's steps 1, 2 and 4 have since been run —
|
||||
[`docker/windows/`](../docker/windows/) is the container, [`packaging/windows/darkroom.nsi`](../packaging/windows/darkroom.nsi)
|
||||
the installer, and §6's gate passes through row 4 under Wine. Four claims in the first draft were
|
||||
wrong and are corrected in place with a note; §10 lists them. Where a claim still rests on reading
|
||||
rather than running, it says so.
|
||||
|
||||
---
|
||||
|
||||
@@ -59,18 +61,29 @@ conventional binary — the same CRT every other Windows application links — 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:
|
||||
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:
|
||||
|
||||
```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"]
|
||||
```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++"
|
||||
```
|
||||
|
||||
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`.
|
||||
**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
|
||||
|
||||
@@ -96,13 +109,14 @@ 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
|
||||
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.
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
@@ -172,19 +186,31 @@ Ordered by what blocks a first sign-in.
|
||||
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.
|
||||
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
|
||||
@@ -252,7 +278,9 @@ $LOCALAPPDATA\Programs\DarkRoom\
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -287,6 +315,11 @@ real but thinly used. Inno Setup runs only under Wine. NSIS is scriptable in pla
|
||||
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
|
||||
@@ -307,8 +340,14 @@ it needs `winevulkan` to find a host ICD, `xvfb`, and a Wine prefix warmed up in
|
||||
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.
|
||||
`--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
|
||||
@@ -396,3 +435,34 @@ its first real test, and the commit that closes §3.2 records the answer.
|
||||
|
||||
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 seven 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).
|
||||
|
||||
**Still open**, in the order §3.2 gives them: the secret store, the known-folder paths, the
|
||||
models lookup beside the executable, the sign-in URL. The binary that exists today starts, and
|
||||
cannot sign in. Then §7's CI leg, whose every step has now been run by hand once.
|
||||
|
||||
Reference in New Issue
Block a user