Files
reforgermods-launcher/docs/distribution.md
T
SowinskiBraedenandClaude Opus 5 377f3a31b3 initial commit: reforgermods.net launcher 0.1.0
A desktop server browser for Arma Reforger, built on the reforgermods.net v2
API. Live server list with search, sort and filters; a detail inspector with
population history and mod readiness; measured ICMP latency; favorites, recents
and settings; and direct joining through Steam.

Tauri 2 and Rust, with a Preact frontend that builds to 80 KB of JavaScript in
one chunk. Logic lives in rfm-core, which has no Tauri dependency and is tested
directly; src-tauri is the window, the command surface and the wiring.

Packaged for the public beta: a per-user NSIS installer, a portable .exe and an
Arch package, with signature-verified background updates on Windows and a
release pipeline that builds from a tag. The artifacts are not yet
Authenticode-signed — that is wired and waiting on an Azure Trusted Signing
identity, and docs/distribution.md says so plainly rather than letting anyone
discover it at the SmartScreen dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 20:29:00 -07:00

343 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Distributing the launcher from reforgermods.net
**Implemented, with one thing outstanding.** The decisions below are made, the
pipeline exists (`.github/workflows/release.yml`) and 0.1.0 is built. The gap is
Authenticode: the release job is wired for Azure Trusted Signing but the
credentials do not exist yet, so the first beta artifacts are **unsigned** and
will show the SmartScreen warning. Everything else — NSIS per-user installer,
signed auto-updates, R2 hosting, the download page — is in place.
See [Release procedure](#release-procedure) for how to cut one, and
[What is still outstanding](#what-is-still-outstanding) for the gap.
## The shape, in one line
An **NSIS per-user installer** as the primary download, built on **Windows in
CI**, signed with **Azure Trusted Signing**, hosted on **Cloudflare R2** at
`dl.reforgermods.net`, with the **Tauri updater** wired so nobody ends up running
a stale API client.
## 1. Installer format
Tauri can produce three Windows artifacts. They are not equivalent:
| Format | Admin needed | Notes |
| --- | --- | --- |
| **NSIS** (`.exe`) | No, with `installMode: currentUser` | Start Menu entry, uninstaller, updater support, smallest |
| **MSI** (WiX) | Yes | Enterprise deployment, Group Policy. Irrelevant for a game tool |
| Portable `.exe` | No | No Start Menu, no uninstaller, SmartScreen prompt every time |
**NSIS, per-user.** It is already configured. Per-user matters: a UAC prompt on
a game utility loses people, and there is no reason to write outside the user's
profile — the launcher already stores everything under the per-user config
directory.
**Done:** `targets` is `["nsis"]`. MSI was removed — it doubled release size and
build time for an audience that will not use it.
The **portable `.exe` is published as a secondary download** for people who will
not run installers. It costs nothing: it is the same binary the installer wraps,
and the release job copies it straight out of the build.
### WebView2
The installer's `webviewInstallMode` decides what happens when the runtime is
missing. `downloadBootstrapper` (the default) adds ~2 MB and fetches the runtime
only when absent. Windows 11 and current Windows 10 already ship it, so most
users never notice. `embedBootstrapper` or `offlineInstaller` only make sense if
offline installs matter.
## 2. Code signing — the decision that actually matters
Unsigned, every download shows **"Windows protected your PC"** and requires
More info → Run anyway. For a launcher whose point is promotion, that is the
single biggest conversion loss in the whole pipeline.
**What changed:** EV certificates used to grant immediate SmartScreen
reputation. **Microsoft removed that in 2024.** An EV certificate now builds
reputation the same way an OV one does, so paying EV prices purely to skip
SmartScreen is no longer justified.
| Option | Cost | Verdict |
| --- | --- | --- |
| Nothing | £0 | SmartScreen warning on every download. Not viable for a promoted tool |
| OV certificate | ~£200–400/yr | Signs fine; reputation still has to build |
| EV certificate | ~£300–600/yr + token/HSM | No longer buys instant reputation. Hard to justify |
| **Azure Trusted Signing** | **$9.99/mo** (5,000 signatures) | Cheapest credible route; no hardware token; fits CI cleanly |
**Recommended: Azure Trusted Signing** (renamed Azure Artifact Signing).
Eligibility is verified US / Canada / EU / UK businesses and self-employed
individuals — `cedarline.digital` should qualify, and the organisation identity
is what appears in the UAC and SmartScreen dialogs.
Be realistic about reputation: signing removes the *unknown publisher* framing,
but SmartScreen reputation still accrues with download volume. Expect some
friction on the first releases regardless of route. The mitigation is to keep
one stable signing identity rather than rotating certificates, so reputation
accumulates against one publisher.
**State:** chosen, wired, not yet active. The release job signs through
`bundle.windows.signCommand`, supplied by `src-tauri/tauri.signing.conf.json` and
run by `packaging/windows/sign.cmd`, which shells out to `trusted-signing-cli`.
Signing has to happen *during* bundling rather than after it, which is why it is
a `signCommand` and not a later CI step: the updater artifact is the installer
plus a detached signature over it. Sign the installer afterwards and the
signature describes a file nobody will download, and every client rejects the
update.
The job checks for `AZURE_CLIENT_ID` and `RFM_SIGN_ENDPOINT` and, when they are
absent, builds unsigned and prints a warning to the run summary rather than
failing. That is deliberate: it keeps the beta shippable while the Azure identity
is sorted out, and it makes the unsigned state loud instead of silent. See
[What is still outstanding](#what-is-still-outstanding).
## 3. Build and release pipeline
Cross-compiling from Linux works for the bare `.exe` (`scripts/build-windows.sh`)
and is fine for development, but the release build should run on Windows:
- NSIS bundling and signing both want Windows tooling.
- The signed artifact should be produced by CI, not a laptop, so releases are
reproducible and the signing credential lives in one place.
**Done:** `.github/workflows/release.yml`. A `windows-latest` job on a `v*` tag
runs the full check suite, builds, signs when it can, and uploads the artifacts;
a second job generates the manifests and publishes to R2. It is stored under
`.github/workflows` so both GitHub Actions and Gitea Actions pick it up, matching
the other two repositories.
The job refuses to build unless the tag and every version in the tree already
agree (`scripts/version.sh check "$tag"`), so a mistagged release fails in the
first thirty seconds rather than after producing artifacts.
**Hosting: Cloudflare R2**, served at `dl.reforgermods.net`. It sits on the
account that already runs the site, keeps the project free of a GitHub
dependency, and downloads never touch the API's budget. The trade is that the
bandwidth is ours; for an installer of about 2 MB that is not a real cost at beta
volumes. See [R2 setup](#r2-setup) for the one-time configuration.
Versioned paths are immutable and only two small files move — `/releases.json`
and `/updates/latest.json` — which is what makes a rollback a matter of
re-publishing one file rather than moving binaries around.
## 4. Auto-update
`tauri-plugin-updater` checks a JSON manifest, verifies a signature against a
public key baked into the build, and applies the update.
This matters more than usual here: the launcher is an API client. When
`/v2/servers` changes shape, users on an old build get a broken browser and
blame the site. An updater turns that from a support problem into a non-event.
Needs a keypair (`tauri signer generate`) — the private key is a CI secret, the
public key ships in `tauri.conf.json`. Distinct from code signing; both are
needed and they solve different problems.
**Done**, in `src-tauri/src/update.rs`. The keypair exists and its public half is
in `tauri.conf.json`; the private key is **not** in this repository and has to be
added to CI as `TAURI_SIGNING_PRIVATE_KEY` (see
[Release procedure](#release-procedure)).
The behaviour is exactly as proposed, and it is three steps rather than
`download_and_install` for a reason:
1. check and download in the background, so nothing blocks startup;
2. hold the downloaded installer until the session ends;
3. run it as the app exits, with `restart_after_install(false)`.
Nobody is interrupted mid-session and nobody is asked to make a decision about a
patch release; the update is simply there next time. `Update::install` calls
`std::process::exit(0)` on Windows after launching the installer, which is
harmless at exit and would be a hard kill at any other moment — which is the
other reason the install is pinned to that exact point.
Every failure path is silent: no endpoint, no network, a malformed manifest or a
bad signature all leave the app as it was. `RFM_DEBUG=1` is how it becomes
visible.
**Windows only.** The dependency is scoped to Windows in `Cargo.toml`, so the
module does not exist in a Linux build. The Arch package belongs to pacman, and
an app that rewrites its own files under `/usr/bin` fights the package manager.
## 5. Download page
**Done**, in the `reforgermods-web` repository: `/launcher/` with
`public/static/launcher.js` and `public/static/launcher/releases.json`.
- Detects the visitor's platform and offers the matching build as the primary
action; an unrecognised platform gets the full table rather than a guess.
- Lists every download with its size and SHA-256.
- States the WebView2 requirement as a footnote — nearly nobody needs it.
- The page ships a working pre-release state in its HTML and the script only ever
upgrades it, so a failed fetch leaves an honest panel rather than a dead button.
The page reads **its own copy** of `releases.json`, served from the site origin
rather than from R2. That avoids a cross-origin dependency on the download host
for a page that must render, but it means a release is not visible until that
file is updated and the site is deployed. The release job says so in its summary.
## 6. Versioning
**Done:** `scripts/version.sh`.
scripts/version.sh show print the canonical version
scripts/version.sh check [tag] fail if any copy disagrees
scripts/version.sh set 0.2.0 rewrite every copy
The canonical value is `[workspace.package] version` in the root `Cargo.toml`,
because that is the one the running binary reports: `rfm-core` builds its
User-Agent from `CARGO_PKG_VERSION`, so the API's per-version rollups are keyed
off it whatever the other files say.
It still has to be repeated in four places — the workspace manifest,
`package.json`, `tauri.conf.json` and the PKGBUILD — because no two of those
tools read each other's format. `check` is what makes the repetition safe, and
the release job runs it against the tag before building anything. An updater
manifest that disagrees with the installed version either loops forever or never
fires, and neither failure is obvious from the outside.
## 7. Linux
Not a distribution channel yet — but the app does build and run there, and
`scripts/build-arch.sh` produces a native Arch package
(`target/arch/*.pkg.tar.zst`) for handing to a tester. It builds inside an
`archlinux:base-devel` container so the binary links Arch's WebKit and glibc
rather than the build host's; `packaging/arch/PKGBUILD` holds the recipe and
README covers the two makepkg workarounds it needs.
Verified end to end: installed into a clean `archlinux:base` container with
`pacman -U`, every dependency resolved from the declared `depends`, and the app
started under Xvfb, loaded the live server list and resolved pings over
unprivileged ICMP.
**Changed for the beta:** the Arch package is now published alongside the Windows
builds rather than handed over per tester. It appears on the download page for
visitors the page detects as Arch, and in the table for everyone else.
That is one distribution, not a Linux strategy. `.deb`, AppImage and an AUR
package are all still unbuilt, and `public/static/launcher.js` already knows
those platform ids so adding them is a packaging job, not a web one. The updater
deliberately does not cover any of them: a distro package is updated by the
distro's tooling, not by the app rewriting its own files in `/usr/bin`.
## Release procedure
One-time setup, then four steps per release.
### One-time
1. **Updater key.** Generated already; the public half is in `tauri.conf.json`.
The private key is at `~/.config/reforgermods-launcher/updater.key` on the
machine it was generated on and **is not in this repository**. Add it to CI:
- `TAURI_SIGNING_PRIVATE_KEY` — the file's contents
- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` — empty; the key was generated without
one, on the grounds that the key is already a secret in the same store and a
password beside it adds a second secret rather than a second factor.
Regenerate with a password if you disagree; both have to change together.
Back it up. Losing it means every installed launcher stops accepting updates,
and the only fix is a new key in a new release that everyone installs by hand.
2. **R2.** See [R2 setup](#r2-setup).
3. **Signing.** See [What is still outstanding](#what-is-still-outstanding).
### Per release
```sh
scripts/version.sh set 0.1.1 # rewrites all four version copies
# update the changelog in reforgermods-web: src/app/launcher/changelog/page.tsx
git commit -am "release 0.1.1" && git tag v0.1.1 && git push --follow-tags
```
The tag starts the pipeline. When it finishes, take `releases.json` from the run
artifacts, drop it into `public/static/launcher/releases.json` in
`reforgermods-web`, and deploy — until then the page still offers the previous
release.
### Building artifacts by hand
The release job is the supported path; these are for testing what it will
produce. Neither can Authenticode-sign, because that needs a Windows host.
```sh
scripts/build-windows.sh --release # the bare .exe
scripts/build-arch.sh # target/arch/*.pkg.tar.zst
```
A full Windows bundle can be cross-built on Linux, but Tauri calls this
experimental and it needs NSIS present:
```sh
export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.config/reforgermods-launcher/updater.key)"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=""
npm run tauri -- build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
```
Debian's `makensis` has `/usr/share/nsis` compiled in as its data directory and
the bundler does not pass `NSISDIR` through, so a rootless install needs a
wrapper on `PATH` that exports it before exec'ing the real binary. 0.1.0 was
built this way and its updater signature verifies against the shipped public key.
Then assemble and describe the result:
```sh
python3 scripts/release-manifest.py --version "$(scripts/version.sh show)" \
--artifacts dist/release --base-url https://dl.reforgermods.net --out dist/release
```
It writes both manifests from one scan of one directory, so the version, the URLs
and the filenames cannot disagree between the download page and the updater. An
artifact with no sibling `.sig` is never offered to the updater: the client would
reject it, and every launch would check, fail and retry.
## R2 setup
One bucket, public over a custom domain:
```sh
npx wrangler r2 bucket create reforgermods-launcher-downloads
```
Then, in the Cloudflare dashboard, attach `dl.reforgermods.net` to the bucket
(R2, the bucket, Settings, Public access, Custom domain). The release job needs
`CLOUDFLARE_API_TOKEN` (Workers R2 Storage: Edit) and `CLOUDFLARE_ACCOUNT_ID` as
secrets on the `release` environment.
Layout:
```text
dl.reforgermods.net/
0.1.0/…-setup.exe installer, and the updater artifact
0.1.0/…-setup.exe.sig detached updater signature
0.1.0/…-0.1.0.exe portable
0.1.0/…-x86_64.pkg.tar.zst Arch package
0.1.0/SHA256SUMS
updates/latest.json what the updater polls
releases.json published for reference; the site reads its own copy
```
Everything under a version prefix is immutable. Only the two manifests move.
## What is still outstanding
**Authenticode signing.** The pipeline is wired and inert. To turn it on:
1. Get a **verified Trusted Signing identity** for `cedarline.digital` and an
Azure subscription that owns it. This is the blocking item — it is an identity
verification, not a purchase, and it takes as long as it takes.
2. Create a Trusted Signing account and certificate profile.
3. Add to the `release` environment — secrets `AZURE_CLIENT_ID`,
`AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`; variables `RFM_SIGN_ENDPOINT`,
`RFM_SIGN_ACCOUNT`, `RFM_SIGN_PROFILE`.
The job picks it up on the next tag with no code change. `packaging/windows/sign.cmd`
has never run on a Windows host, so budget one throwaway tag to shake it out
rather than discovering it on a release you meant to publish.
Until then the beta is unsigned and every download shows **"Windows protected
your PC"**. The download page says so plainly rather than letting people discover
it at the SmartScreen dialog — an honest note costs less trust than a surprise.