feat(installer): implement Phase 1 — the installer core #4

Merged
whitlocktech merged 1 commits from feat/phase1-installer-core into edge 2026-08-04 20:02:39 +00:00
Member

What & why

Phase 1 of docs/installer/PLAN.md — the installer core. Adds the Rust crate at the repo root and implements install end to end for the overlay half of a deployment:

  • resolve the published bundle (bundles/current.json, or --bundle <tag> to pin one),
  • detect / validate the ServUO root, read its version, and refuse to deploy under a running shard,
  • download the overlay tarball, verify its SHA256, unpack it, re-verify every file against the release's own manifest.json,
  • sync into the tree — add and overwrite only, never delete — with Config/Bridge.cfg reported, not clobbered once the operator has edited it,
  • write install.json, and re-run idempotently.

doctor / update / uninstall parse and answer with the phase they arrive in rather than "unrecognized command". The run also states what it did not do: the sidecar (Phase 2) is named as not installed, with the bundle's binary URL + SHA256 and a pointer to INSTALL.md A3/A4, and --patches reports REQUESTED BUT NOT APPLIED — no stock ServUO file has been touched, because a --patches run that completed quietly would be read as a patched shard.

Targets edge, not main, on purpose. release.yml publishes an installer binary on every push to main, and a binary that deploys the overlay but cannot install the sidecar is not something to hand an operator. Phases 1 and 2 land on edge; the edge → main cutover cuts the first release. pr-checks.yml now gates PRs into edge on the same rules, so the branch the work happens on is not the ungated one. Neither workflow needed a temporary edit — both already stand down when there is no root Cargo.toml.

Decisions worth a reviewer's attention

  • The library target is rgdeploy, not runicgateway_installer. Windows applies UAC installer detection to unsigned executables whose file name contains install: it demands elevation before the process starts, and a non-interactive session gets os error 740 instead of a program. Cargo names test harnesses after their target, so a target under that name makes cargo test unrunnable on Windows — the machine the shard smoke tests live on. The binary target keeps its published name and [[bin]] test = false stops Cargo building a harness under it. Nothing an operator sees changes; INSTALL.md gains a troubleshooting row for the elevation prompt.
  • The running-shard check matches by path, not process name. deploy.ps1 looks for a process called ServUO because it only runs on Windows; on Linux a live shard is mono/dotnet with ServUO.exe as an argument, and a name match would answer "not running" for a shard that is running — the one wrong answer that corrupts Scripts.dll. A match needs a process naming both this tree and ServUO.exe, so another shard on the host does not block the deploy and the installer never matches itself.
  • install.json records a state, not a verb (deployed / kept-operator-modified). Recording add then unchanged made an identical re-run produce a different record, which rewrote the file every run and broke "a second run writes nothing" invisibly.
  • The Bridge.cfg keep compares against the last hash deployed, not the last hash seen. After a keep, the record's on-disk hash is the operator's content — so "is the tree still what the record last saw?" matches on the very next run and overwrites exactly the file the rule protects. Both of these were found by the live smoke run, not by the unit tests, and both now have tests.
  • Dependencies picked for the MinGW cross-build: ureq (blocking HTTP over rustls/ring — no OpenSSL, no async runtime for four sequential requests), flate2 on its pure-Rust backend, tar, sha2, serde, chrono, anyhow, sysinfo.

Docs half: docs#90 (docs(installer): record Phase 1 as built).

How it was tested

cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test — 50 tests, all green.

End to end, with the binary copied under a neutral name (see the UAC note above):

  • --verify against a real ServUO 57.4 tree (C:\Users\colby\Desktop\ServUO): bundle fetched over the network, tarball downloaded and checksum-verified, version detected as 57.4, and the tree read correctly — its Bridge.cfg reported as operator-owned, 23 code files as changed, nothing written.
  • Full install into a scratch tree: 24 files deployed (matching INSTALL.md §3), install.json written with 24 per-file entries.
  • Idempotency: second run reports unchanged. add=0 change=0 unchanged=24 kept=0 and leaves install.json untouched (mtime unchanged).
  • Ownership rules: an edited Bridge.cfg stays the operator's across three further runs while a hand-edited .cs is overwritten every time.
  • --bundle 2026.08.04 pins; a nonexistent tag fails with the "every bundle is kept forever" message, exit 1.
  • Running shard: with a process running out of the tree, the run refuses with pid and path and exit 1.
  • Exit codes: 2 for an unrecognized flag, 1 for an unimplemented command (naming Phase 4 and a manual workaround).

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • AI tools were used. Tool(s): Claude Code. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why Phase 1 of [`docs/installer/PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/PLAN.md) — the installer core. Adds the Rust crate at the repo root and implements `install` end to end for the overlay half of a deployment: - resolve the published bundle (`bundles/current.json`, or `--bundle <tag>` to pin one), - detect / validate the ServUO root, read its version, and **refuse to deploy under a running shard**, - download the overlay tarball, verify its SHA256, unpack it, re-verify every file against the release's own `manifest.json`, - sync into the tree — add and overwrite only, never delete — with `Config/Bridge.cfg` **reported, not clobbered** once the operator has edited it, - write `install.json`, and re-run idempotently. `doctor` / `update` / `uninstall` parse and answer with the phase they arrive in rather than "unrecognized command". The run also states what it did **not** do: the sidecar (Phase 2) is named as not installed, with the bundle's binary URL + SHA256 and a pointer to INSTALL.md A3/A4, and `--patches` reports `REQUESTED BUT NOT APPLIED — no stock ServUO file has been touched`, because a `--patches` run that completed quietly would be read as a patched shard. **Targets `edge`, not `main`, on purpose.** `release.yml` publishes an installer binary on every push to `main`, and a binary that deploys the overlay but cannot install the sidecar is not something to hand an operator. Phases 1 and 2 land on `edge`; the `edge → main` cutover cuts the first release. `pr-checks.yml` now gates PRs into `edge` on the same rules, so the branch the work happens on is not the ungated one. Neither workflow needed a temporary edit — both already stand down when there is no root `Cargo.toml`. ### Decisions worth a reviewer's attention - **The library target is `rgdeploy`, not `runicgateway_installer`.** Windows applies UAC *installer detection* to unsigned executables whose file name contains `install`: it demands elevation before the process starts, and a non-interactive session gets `os error 740` instead of a program. Cargo names test harnesses after their target, so a target under that name makes `cargo test` **unrunnable on Windows** — the machine the shard smoke tests live on. The binary target keeps its published name and `[[bin]] test = false` stops Cargo building a harness under it. Nothing an operator sees changes; INSTALL.md gains a troubleshooting row for the elevation prompt. - **The running-shard check matches by path, not process name.** `deploy.ps1` looks for a process called `ServUO` because it only runs on Windows; on Linux a live shard is `mono`/`dotnet` with `ServUO.exe` as an argument, and a name match would answer "not running" for a shard that is running — the one wrong answer that corrupts `Scripts.dll`. A match needs a process naming *both* this tree and `ServUO.exe`, so another shard on the host does not block the deploy and the installer never matches itself. - **`install.json` records a state, not a verb** (`deployed` / `kept-operator-modified`). Recording `add` then `unchanged` made an identical re-run produce a different record, which rewrote the file every run and broke "a second run writes nothing" invisibly. - **The `Bridge.cfg` keep compares against the last hash *deployed*, not the last hash *seen*.** After a keep, the record's on-disk hash is the operator's content — so "is the tree still what the record last saw?" matches on the very next run and overwrites exactly the file the rule protects. Both of these were found by the live smoke run, not by the unit tests, and both now have tests. - **Dependencies picked for the MinGW cross-build:** `ureq` (blocking HTTP over rustls/ring — no OpenSSL, no async runtime for four sequential requests), `flate2` on its pure-Rust backend, `tar`, `sha2`, `serde`, `chrono`, `anyhow`, `sysinfo`. Docs half: [docs#90](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/90) (`docs(installer): record Phase 1 as built`). ## How it was tested `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test` — 50 tests, all green. End to end, with the binary copied under a neutral name (see the UAC note above): - **`--verify` against a real ServUO 57.4 tree** (`C:\Users\colby\Desktop\ServUO`): bundle fetched over the network, tarball downloaded and checksum-verified, version detected as 57.4, and the tree read correctly — its `Bridge.cfg` reported as operator-owned, 23 code files as changed, nothing written. - **Full install into a scratch tree**: 24 files deployed (matching INSTALL.md §3), `install.json` written with 24 per-file entries. - **Idempotency**: second run reports `unchanged. add=0 change=0 unchanged=24 kept=0` and leaves `install.json` untouched (mtime unchanged). - **Ownership rules**: an edited `Bridge.cfg` stays the operator's across three further runs while a hand-edited `.cs` is overwritten every time. - **`--bundle 2026.08.04`** pins; a nonexistent tag fails with the "every bundle is kept forever" message, exit 1. - **Running shard**: with a process running out of the tree, the run refuses with pid and path and exit 1. - **Exit codes**: `2` for an unrecognized flag, `1` for an unimplemented command (naming Phase 4 and a manual workaround). ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [x] AI tools were used. Tool(s): `Claude Code`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-04 19:59:41 +00:00
feat(installer): implement Phase 1 — the installer core
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m31s
dff4ad41c9
Adds the Rust crate at the repo root and implements `install` end to end for
the overlay half of a deployment: resolve the published bundle, find and
validate the ServUO root, refuse to deploy under a running shard, sync the
plugin overlay, and record what was deployed in install.json.

`doctor`, `update` and `uninstall` parse and answer with the phase they arrive
in rather than "unrecognized command", and the run states plainly that the
uo-link sidecar (Phase 2) and the patch tier (Phase 3) were not installed —
`--patches` in particular reports REQUESTED BUT NOT APPLIED, since a quiet
completion would be read as a patched shard.

Landing on `edge` rather than `main`: release.yml publishes a binary on every
push to main, and an installer that deploys the overlay but cannot install the
sidecar is not something to hand an operator. pr-checks.yml now gates PRs into
edge on the same rules, so the branch the work happens on is not the ungated
one.

Notable decisions, all documented in docs/installer/PLAN.md §5 Phase 1:

- The code lives in a library called `rgdeploy` with a thin binary that keeps
  the published name. Windows' UAC installer detection refuses to launch an
  unsigned executable whose file name contains "install" (os error 740), and
  Cargo names test harnesses after their target — so a target under that name
  makes `cargo test` unrunnable on Windows.
- The running-shard check matches processes by path, not by process name:
  on Linux a live shard is `mono`/`dotnet` with ServUO.exe as an argument, and
  a name match would report "not running" for a shard that is running.
- install.json records a state (`deployed` / `kept-operator-modified`), not the
  run's verb, so an unchanged re-run produces an identical record and writes
  nothing.
- The Bridge.cfg keep rule compares against the hash the installer last
  deployed, not the last hash it saw — otherwise a kept file is overwritten on
  the very next run.
- Downloads are verified against the bundle's SHA256 while being written, then
  every extracted file is re-hashed against the release's own manifest.json,
  whose protocol and version are cross-checked against the bundle.

Verified against a real ServUO 57.4 tree and end to end into a scratch tree:
24 files deployed, an unchanged re-run that writes nothing, an edited
Bridge.cfg kept across repeated runs while code files are overwritten, bundle
pinning, and a refusal with a shard running out of the tree.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech approved these changes 2026-08-04 20:02:16 +00:00
whitlocktech merged commit a4f6b7b756 into edge 2026-08-04 20:02:39 +00:00
whitlocktech deleted branch feat/phase1-installer-core 2026-08-04 20:02:39 +00:00
Sign in to join this conversation.
No description provided.