diff --git a/installer/PLAN.md b/installer/PLAN.md index 449b352..d2edf7d 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -17,7 +17,10 @@ release layout is exactly what Phase 5 changes. So the `edge → main` cutover i things: 1. **Phase 5, packaging polish** (§5) — the last work before a first release, rather than the first - work after it. + work after it. Its scope is now settled: **no `.deb` and no MSI** (§5.1 — both would give the + service, its unit and its user a second owner), **Linux `aarch64` for both components** (§5.2), + **a backup of what an upgrade overwrites** (§5.3), and the docs a first release invalidates + (§5.4). 2. **The Windows SCM half being verified on a real host.** systemd registration has now been run for real (see [Phase 2 as built](#phase-2--uo-link-install-and-service)), and doing it found a bug no amount of unit testing had. `sc create`, the virtual service account, the failure actions and the @@ -277,9 +280,11 @@ operators. - **`servuo-plugins` had no release workflow.** Only `link` did. "Pull latest repository" is replaced by a release tarball, which had to be built first — Phase 0 item 1, now in review ([servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7)). -- **arm64 is not buildable today.** `link/release.yml` cross-compiles only - `x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`. An arm64 `.deb` needs another cross - toolchain. +- **arm64 was not buildable today.** `link/release.yml` cross-compiled only + `x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`, so `platform_key()` refused every other + host by name. That was a property of the workflows, not of Rust — Phase 5 adds + `aarch64-unknown-linux-gnu` to both components in the order §5.2 sets out. There is no `.deb`, so + the cross toolchain is the whole cost. - **The compat matrix has no home.** `PROTOCOL_VERSION` lives in `link/sidecar/src/main.rs`. The sidecar publishes it via `X-UOLink-Version` and `/health`, and the website stores an expected value — but the *plugin's* protocol version is not queryable before boot. Phase 0 item 1 gives it @@ -296,12 +301,13 @@ Components are published as Gitea release artifacts. Operators download from the Runic Gateway Installer v1.0.0 ├── runicgateway-installer-windows-x86_64.exe ├── runicgateway-installer-linux-x86_64 +├── runicgateway-installer-linux-aarch64 (Phase 5) └── SHA256SUMS uo-link v1.1.0 (existing release, extended) ├── uo-link-sidecar-windows-x86_64.exe ├── uo-link-sidecar-linux-x86_64 -├── runicgateway-link__amd64.deb (Phase 5) +├── uo-link-sidecar-linux-aarch64 (Phase 5) └── SHA256SUMS servuo-plugins v (new release, Phase 0) @@ -794,7 +800,7 @@ a clever automatic revert risks silently eating their work. It removes and it re | Action | Scope | |---|---| | Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` | -| Kept | `sidecar.toml`, `uo-link.db`, and the cached patch set with its pre-patch originals (`--purge` to drop them) | +| Kept | `sidecar.toml`, `uo-link.db`, the cached patch set with its pre-patch originals, and (from Phase 5) the backups an upgrade took — `--purge` to drop them | | **Printed, not done** | Every overlay file deployed into the ServUO tree, listed by path, for the operator to delete | | **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files — with the rung that applied each one (§2.2.1), since a `region-match` apply means the surrounding file was already the operator's — for them to revert by hand | @@ -876,21 +882,138 @@ Windows host, since only half of `service.rs` compiles on either. ### Phase 5 — packaging polish -`.deb` packaging, Windows MSI, arm64 cross build, and optional automated backup before upgrade. -Deliberately last *of the build phases*: v1 can register services directly (`sc create` / a written -systemd unit) and ship plain binaries. Nothing in Phases 1–4 should have to change to add these. +The last work before the first release. It was sketched as four items — `.deb` packaging, Windows +MSI, arm64 cross build, optional automated backup before upgrade — and the org lead settled its scope +on 2026-08-05: **two of the four are dropped rather than deferred**, because what stops them is an +ownership conflict that does not improve with time, and two are built. -**It now runs before the `edge → main` cutover rather than after it** (org lead, 2026-08-05). The -original order assumed the cutover would cut a v1 and packaging would follow as a v1.x — but this -phase changes the *release layout* (§3), so shipping first would mean a first release that is -superseded by the next one, and operators who downloaded a bare binary being told to re-download a -package. Deferring the cutover costs nothing: nothing is published from `edge`, and the guide's -Appendix A is the supported path meanwhile. +**It runs before the `edge → main` cutover rather than after it.** The original order assumed the +cutover would cut a v1 and packaging would follow as a v1.x — but this phase changes the *release +layout* (§3), so shipping first would mean a first release that is superseded by the next one, and +operators who downloaded a bare binary being told to re-download a package. Deferring the cutover +costs nothing: nothing is published from `edge`, and the guide's Appendix A is the supported path +meanwhile. -The two entry criteria for the cutover are therefore this phase and the outstanding **Windows SCM +| Item | Decision | +|---|---| +| `.deb` for uo-link | **Dropped** (§5.1) | +| Windows MSI | **Dropped** (§5.1) | +| arm64 cross build | **Build** — Linux `aarch64`, both components (§5.2) | +| Backup before upgrade | **Build** — on by default, scoped to what a run overwrites (§5.3) | + +#### 5.1 What ships is plain binaries: no `.deb`, no MSI + +Both were proposed before Phases 2 and 4 existed. They now collide with the components those phases +made the installer own, and the collision is the deciding argument in each case. + +- **A `.deb` under link's release would own `/usr/bin/runicgateway-link`, the systemd unit and the + service user** — the same three things `service.rs` writes, hardens and removes, and that + `install.json` records so `doctor` and `uninstall` can reason about them. Two owners for one file + is not a packaging detail: `uninstall` deleting a dpkg-owned binary leaves the package *installed* + and broken, an `apt upgrade` replacing that binary makes `doctor` report drift no operator caused, + and the unit text would live in two repos free to disagree about the account it runs as. The + variant that avoids all of that — a binary-only `.deb`, no unit, no user — buys apt-managed + upgrades of one file, which `update` already does from a **protocol-checked** bundle (§7.1). That + is the stronger of the two guarantees, so the package would be trading correctness for + familiarity. +- **An MSI contradicts a decision already locked**: the installer does not install itself (Phase 0.4 + as built). It would also add a second uninstall path — Add/Remove Programs — beside the + `uninstall` verb, which owns state an MSI cannot see: `install.json`, the cached patch set and its + pre-images, and the ServUO-tree report that is printed rather than done (Phase 4). Unsigned, it + raises the same SmartScreen prompt a bare `.exe` does (§3), so it does not even buy the dialog it + looks like it should. + +So §3's release layout loses its `.deb` line and gains no installer package. Nothing in Phases 1–4 +changes — this item is a removal, which is why settling it is cheap and shipping it first would not +have been. + +**What would reopen it.** If operators ask for `apt`- or `winget`-managed installs, the thing to +package is the sidecar as a standalone service *with the installer taught to detect and defer to a +package-managed one*. That is a design change to who owns the service, not packaging polish, and it +is out of scope here. + +#### 5.2 arm64 — Linux `aarch64`, both components + +§2.6 recorded arm64 as "not buildable today", which was true of the *workflows* rather than of Rust: +`link/release.yml` and `installer/release.yml` cross-compile `x86_64` Linux and Windows only, so +`platform_key()` (`src/bundle.rs`) refuses every other host by name. Ampere/Graviton instances and +Pi-class boxes are a realistic ServUO home, and the installer's dependency set was already chosen to +avoid OpenSSL and any C toolchain of its own (Phase 1 as built) — which is what makes this two build +steps per workflow rather than a toolchain project. + +**Both components or neither.** An installer that runs on `aarch64` but resolves a bundle carrying no +`aarch64` sidecar has moved the failure later, not fixed it. Windows-on-arm and macOS stay unbuilt: +no supported MinGW target here for the first, and no shard host is the second. + +**The order is forced by the bundle CI, which is strict on purpose** (Phase 0.3 as built): an +unrecognized `link` asset name is a hard failure, and `linux-x86_64` + `windows-x86_64` are asserted +present. So a link release carrying a new binary reddens the compose job unless it is taught the name +first — and requiring the name before link publishes it fails *every* bundle for as long as the gap +lasts. Hence: + +| Step | Repo / branch | Change | Why it is this one first | +|---|---|---|---| +| 1 | `installer` `main` | `bundle.yml` maps `*-linux-aarch64` → `linux-aarch64`, and **does not** add it to the required list | The compose job runs from `main`; teaching it the name first means link's next release composes instead of failing | +| 2 | `link` `main` | `release.yml` adds `aarch64-unknown-linux-gnu`, cross-linked with `gcc-aarch64-linux-gnu` | Publishes the first `aarch64` sidecar; the nightly cron folds it into a bundle | +| 3 | `installer` `main` | `linux-aarch64` joins the required list | Only safe once a release actually carries it — from here a dropped target reddens CI instead of silently vanishing from every bundle | +| 4 | `installer` `edge` | `release.yml` adds the same target; `platform_key()` learns `("linux", "aarch64")` | The installer binary itself, on the branch that carries the crate | + +`bundle.yml` is therefore edited on `main` only and never on `edge`, so the cutover merge has nothing +to conflict over. + +#### 5.3 Backup before overwrite + +**Scoped by what cannot be fetched again.** Not the sidecar binary or the overlay files — both are +re-downloadable and hash-named in the bundle. Not the database either: `link/sidecar/src/store.rs` +creates every table `IF NOT EXISTS` and every one of them holds shard state the sweeps repopulate, so +it is a cache with a schema rather than a record. What an `update` can destroy irrecoverably is: + +1. **An operator's own edits to a deployed `.cs` file.** Phase 1 overwrites those unconditionally and + by design — `Bridge.cfg` is the single exception — so the one place the tool knowingly discards + work is the one place it should keep a copy. +2. **`sidecar.toml`**, whose token the website already holds. Mint a new one and the site's saved + config starts answering `409`/`401` with nothing on the sidecar to explain why (§2.4). + +So the rule is **copy what this run is about to overwrite, plus `sidecar.toml`** — not a snapshot of +everything. A snapshot of all 24 overlay files would be mostly byte-identical to a release tarball +that is still downloadable, and the noise would bury the two files that matter. + +- **Where:** `/backups//`, each file mirroring its path in the tree it came + from, beside a `manifest.json` naming the source path, its SHA256, and the bundle moved from and + to. In the state directory, not the ServUO tree — the same rule the tier's `patches/originals/` + follows, and for the same reason: `uninstall` has promised never to clean up in there. +- **When:** on by default for `update`, and for an `install` over an existing record. A first + install overwrites nothing and writes no backup. `--verify` writes none either, for the reason it + runs no part of the sidecar half (Phase 2): a dry run must not create state. +- **`--no-backup`** opts out, for an operator with their own snapshotting. +- **Retention is three.** Older ones are pruned as new ones are written; an unbounded directory of + ServUO source copies on a shard host is its own support problem. `uninstall` keeps them and + `--purge` drops them, alongside the config, the database and the cached patch set — same rule as + Phase 4, and the same reason: they are the only offline record of what was there before. +- **Restore is printed, not done.** Consistent with the uninstall report, and for the identical + reason: the installer cannot know what has changed since, and a clever automatic restore over a + newer overlay eats work rather than saving it. `doctor` names the most recent backup and its + timestamp, so an operator asking "can I go back" does not have to know the layout to answer. + +#### 5.4 Documentation the release layout invalidates + +Not optional, and grouped here because a first release is the moment these are read for the first +time by someone who was not in the room: + +- **`installer/README.md`'s status table still says Phase 1 is built and Phase 2 is next**, four + phases later. It is the first thing a visitor to the repo reads. +- **`INSTALL.md`** gains the `aarch64` download lines, the backup behaviour and `--no-backup`. +- **This file:** §3 loses the `.deb` and gains the `aarch64` assets, §2.6's arm64 bullet is now + historical, and §7.1's example bundle grows the third asset key. + +#### Cutover entry criteria + +The `edge → main` cutover is gated on this phase **and** on the outstanding **Windows SCM verification** (see the status header and [Phase 2 as built](#phase-2--uo-link-install-and-service)). The second is not busywork: running the systemd half for real found a bug that unit tests could not, -and `sc create` remains the only code in the crate that has never executed. +and `sc create` remains the only code in the crate that has never executed. The smoke script at the +workspace root was run on 2026-08-05 and stopped at its first check — an unelevated shell — so +nothing in that half has executed yet; it needs one run from an elevated PowerShell. --- @@ -1016,6 +1139,7 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi "repo": "RunicGateway/link", "tag": "v1.1.0", "version": "1.1.0", "protocol": 3, "assets": { "linux-x86_64": { "name": "uo-link-sidecar-linux-x86_64", "url": "…", "sha256": "27d491ef…" }, + "linux-aarch64": { "name": "uo-link-sidecar-linux-aarch64", "url": "…", "sha256": "…" }, "windows-x86_64": { "name": "uo-link-sidecar-windows-x86_64.exe", "url": "…", "sha256": "fbefd886…" } } }, @@ -1030,7 +1154,8 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi Note `link.assets` is a **map keyed by platform**, not the single `sha256` this section originally sketched: link publishes a Linux binary and a Windows `.exe`, and the installer runs on both, so one -hash could only ever have described one of them. `schema` versions this document's shape and is +hash could only ever have described one of them. Phase 5's `linux-aarch64` (§5.2) is the first +addition that map was shaped for, and it costs the bundle nothing but a key. `schema` versions this document's shape and is independent of `protocol` and of either component's release version — all three move separately. The installer fetches the current bundle at run time; `--bundle ` pins an older one for a