Files
installer/Cargo.toml
wtclaude 52d330167b
Some checks failed
PR Checks / rust-gates (pull_request) Failing after 46s
feat(installer): implement Phase 3 — the patch tier
Two features need edits to stock ServUO sources, because the events they
depend on do not exist. This adds the rung ladder of PLAN.md §2.2.1, the
unsupported-version path of §2.2.2, and the record and cache Phase 4 will read.

Three decisions were not settled by the plan:

* The engine is fully native, with no `git`. §2.2.1 wrote rung 1 as "apply
  verbatim with git apply", but §1 chose the release tarball specifically so
  there would be no git on the shard host, and rung 2 needs a native applier
  regardless. Rung 1 keeps its distinct, stronger verdict — the whole file
  reproduced the diff's `index` pre-image, computed as a git blob SHA1 in
  process — while the write goes through the same code path as rung 2. On the
  real trees here that is not academic: the shipped .patch files are CRLF in a
  Windows checkout and two of their three targets are LF, so `git apply`
  refuses patches this applies correctly.

* Per-patch metadata is declared by the release, with a built-in fallback.
  Which patches form one all-or-nothing unit, which companion .cs follows
  which, whether a CORE rebuild is needed and what declining costs are not
  derivable from a diff. servuo-plugins now declares them; overlay v0.1.1 is in
  the current bundle and declares nothing, so a built-in copy stands in for it.
  A checked-in fixture of the release workflow's own jq output asserts the two
  descriptions are identical, so the repos cannot drift quietly.

* Pre-images are cached in the state directory. The tier edits files the
  operator owns, and `/etc/runicgateway/patches/originals/` is what turns "here
  are the hunks we added" into a revert anyone can verify — kept out of the
  ServUO tree, which uninstall has promised never to clean up.

Everything else follows §2.2.1: exact matching with only line-ending and
trailing-whitespace normalization, exactly one occurrence or it fails,
all-or-nothing per patch file and again per feature, and a byte-preserving
splice so nothing outside a hunk can be reformatted.

Verified against the ServUO 57.4 tree on this machine across four scratch
roots: a hand-patched tree (rung 0), a reverse-applied stock one (rung 1 on the
real EventSink.cs, its blob matching the patch's declared pre-image), a
mixed-rung feature, a tree with edits inside two patched regions (rung 3 —
nothing written, nothing held back applied, no companions copied), and a
non-57.4 tree both with and without the extra consent flag. Three consecutive
runs left install.json byte-identical and the cached pre-image still pre-patch.

Three reporting defects the live runs caught are fixed with tests: a dry run
and a held-back patch both claimed to be "applied", the core-rebuild warning
fired when nothing had been written and named a Scripts file as core, and a
declined tier announced the loss of features install.json showed as applied.
Refused patches are now cached too, since the refusal message names that path.

Refs: docs/installer/PLAN.md §2.2, §5 Phase 3

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 19:54:36 -05:00

67 lines
2.9 KiB
TOML

[package]
name = "runicgateway-installer"
version = "0.1.0"
edition = "2021"
description = "Deployment tool for Runic Gateway: syncs the ServUO plugin overlay, installs the uo-link sidecar, and records what it deployed."
license = "GPL-3.0-or-later"
repository = "https://gitea.whitlocktech.com/RunicGateway/installer"
# The published binary keeps the name PLAN.md §3 and INSTALL.md give it. The library it is built
# from does not share that name on purpose: 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 called `runicgateway_installer` makes `cargo test`
# unrunnable on Windows. `test = false` keeps Cargo from building a harness under the binary's
# name; all the code, and all the tests, live in the library. See src/lib.rs.
[lib]
name = "rgdeploy"
path = "src/lib.rs"
[[bin]]
name = "runicgateway-installer"
path = "src/main.rs"
test = false
[dependencies]
# Blocking HTTP over a pure-Rust TLS stack (rustls + ring + webpki-roots). The
# release cross-compiles to x86_64-pc-windows-gnu through MinGW, where anything
# linking OpenSSL turns a one-line build into a toolchain project — and this tool
# makes a handful of sequential requests, so an async runtime would be overhead
# with nothing to overlap.
ureq = "3.3"
# Overlay releases ship as gzipped tar. flate2's default backend is miniz_oxide
# (pure Rust), so it cross-compiles with no C dependency of its own.
flate2 = "1"
tar = "0.4"
# SHA256 is the entire trust anchor for these deliberately unsigned artifacts
# (PLAN.md §3), which makes this load-bearing rather than a nicety.
sha2 = "0.11"
# SHA1 is here for one reason only: a patch's `index <old>..<new>` line carries
# git blob hashes, and reproducing one is how the patch tier answers rung 1 —
# "is this whole file still stock?" (PLAN.md §2.2.1). It is never used as a
# security primitive. Computing it natively is what keeps `git` off the shard
# host, which is the whole point of shipping the plugin as a release tarball.
sha1 = "0.11"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# RFC 3339 timestamps for install.json. Same feature set link's sidecar uses.
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
# Refusing to deploy under a running shard is a correctness requirement, not a
# courtesy: ServUO holds Scripts.dll open and rewrites Saves/ on exit. Only the
# `system` feature is wanted — disks, networks and users are not our business.
sysinfo = { version = "0.38", default-features = false, features = ["system"] }
# Error plumbing, as in link's sidecar. Every failure here is read by an operator
# rather than matched on, so a chain of `.context()` strings is the whole
# requirement — the value is that "failed to write install.json" arrives with the
# path and the OS error attached instead of alone.
anyhow = "1"
[profile.release]
opt-level = 2