diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index fdb3457..627b169 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -66,6 +66,9 @@ env: BIN: runicgateway-installer LINUX_TARGET: x86_64-unknown-linux-gnu WINDOWS_TARGET: x86_64-pc-windows-gnu + # The installer has to run wherever the sidecar it installs can run, and link + # publishes an arm64 Linux binary from v1.2.0 (PLAN.md §5.2, step 4 of 4). + ARM64_TARGET: aarch64-unknown-linux-gnu jobs: release: @@ -237,7 +240,7 @@ jobs: echo "Release credentials present." # ── RUST ADAPTER: toolchain + cross-compile deps ───────────────────── - - name: Install Rust toolchain, Windows target, and MinGW linker + - name: Install Rust toolchain, cross targets, and their linkers if: ${{ steps.plan.outputs.release == 'true' }} run: | set -euo pipefail @@ -254,6 +257,7 @@ jobs: export PATH="${HOME}/.cargo/bin:${PATH}" rustup component add rustfmt rustup target add "${WINDOWS_TARGET}" + rustup target add "${ARM64_TARGET}" - name: Set the crate version to match the release if: ${{ steps.plan.outputs.release == 'true' }} @@ -299,8 +303,12 @@ jobs: run: | set -euo pipefail cp "target/${LINUX_TARGET}/release/${BIN}" "dist/${BIN}-linux-x86_64" + cp "target/${ARM64_TARGET}/release/${BIN}" "dist/${BIN}-linux-aarch64" cp "target/${WINDOWS_TARGET}/release/${BIN}.exe" "dist/${BIN}-windows-x86_64.exe" - ( cd dist && sha256sum "${BIN}-linux-x86_64" "${BIN}-windows-x86_64.exe" > SHA256SUMS ) + # Every artifact must be listed: `sha256sum -c` passes silently over a + # file the sums do not mention, and an operator verifying a download + # would get a pass on a binary nobody vouched for. + ( cd dist && sha256sum "${BIN}-linux-x86_64" "${BIN}-linux-aarch64" "${BIN}-windows-x86_64.exe" > SHA256SUMS ) ls -l dist && echo "----" && cat dist/SHA256SUMS # ── RELEASE ENGINE: commit the bump, tag, push ─────────────────────── @@ -365,7 +373,7 @@ jobs: | jq -r '.id')" echo "Created release ${TAG} (id=${REL_ID})" - for f in "${BIN}-linux-x86_64" "${BIN}-windows-x86_64.exe" SHA256SUMS; do + for f in "${BIN}-linux-x86_64" "${BIN}-linux-aarch64" "${BIN}-windows-x86_64.exe" SHA256SUMS; do curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \ -H "Authorization: token ${CI_TOKEN}" \ -F "attachment=@dist/${f}" >/dev/null diff --git a/src/bundle.rs b/src/bundle.rs index 59ff818..c36c5da 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -43,8 +43,10 @@ pub struct LinkComponent { pub tag: String, pub version: String, pub protocol: u32, - /// Keyed by platform (`linux-x86_64`, `windows-x86_64`) — link publishes a binary per OS and - /// the installer runs on both, so a single hash could only ever describe one of them. + /// Keyed by platform (`linux-x86_64`, `linux-aarch64`, `windows-x86_64`) — link publishes a + /// binary per target and the installer runs on each, so a single hash could only ever describe + /// one of them. The set grows over time, so a bundle is not expected to carry every key this + /// binary knows about: an older one pinned with `--bundle` predates arm64 entirely. pub assets: BTreeMap, } @@ -85,7 +87,10 @@ impl Bundle { let key = platform_key()?; self.link.assets.get(key).ok_or_else(|| { anyhow::anyhow!( - "bundle {} has no uo-link binary for {key} (it has: {})", + "bundle {} has no uo-link binary for {key} (it has: {}).\n\ + Bundles published before uo-link built for this platform cannot gain one \ + retroactively — they are kept unchanged so `--bundle` stays reproducible. \ + Run without `--bundle` to take the current one.", self.bundle, self.link .assets @@ -102,12 +107,16 @@ impl Bundle { pub fn platform_key() -> Result<&'static str> { match (std::env::consts::OS, std::env::consts::ARCH) { ("linux", "x86_64") => Ok("linux-x86_64"), + // Ampere/Graviton and Pi-class hosts (PLAN.md §5.2). Linux only: the shard dials the + // sidecar out on loopback, so the pair has to be co-located, and no ServUO host is a + // Windows-on-arm box or a Mac. + ("linux", "aarch64") => Ok("linux-aarch64"), ("windows", "x86_64") => Ok("windows-x86_64"), - // arm64 is not buildable today (PLAN.md §2.6) and macOS is not a target. Saying so beats - // failing later with a missing-key error that reads like a corrupt bundle. + // Naming the platforms that do exist beats failing later with a missing-key error that + // reads like a corrupt bundle. (os, arch) => bail!( "no Runic Gateway build exists for {os}/{arch}. \ - The released components target linux-x86_64 and windows-x86_64." + The released components target linux-x86_64, linux-aarch64 and windows-x86_64." ), } } @@ -190,18 +199,56 @@ mod tests { assert_eq!(bundle.link.version, "1.1.0"); assert_eq!(bundle.overlay.version, "0.1.1"); assert_eq!(bundle.overlay.servuo.patches_verified_against, "57.4"); - assert_eq!(bundle.link.assets.len(), 2); assert!(bundle.overlay.asset.name.ends_with(".tar.gz")); } #[test] - fn both_platforms_have_a_sidecar_binary() { - // Whichever of the two this test runs on, the lookup must resolve — a bundle missing the - // host's binary would fail an install after the overlay had already been deployed. + fn every_platform_the_bundle_names_is_well_formed() { + // A floor, not an exact count: `linux-aarch64` joins these from link's first arm64 release + // (PLAN.md §5.2), and a test asserting "exactly two" would fail on the bundle that adds it + // rather than on anything being wrong. let bundle = parse(CURRENT).unwrap(); - let asset = bundle.sidecar_asset().unwrap(); - assert_eq!(asset.sha256.len(), 64); - assert!(asset.url.contains(&bundle.link.tag)); + for required in ["linux-x86_64", "windows-x86_64"] { + let asset = bundle + .link + .assets + .get(required) + .unwrap_or_else(|| panic!("bundle carries no {required} binary")); + assert_eq!(asset.sha256.len(), 64); + assert!(asset.url.contains(&bundle.link.tag)); + } + } + + #[test] + fn the_hosts_binary_either_resolves_or_says_why_not() { + // On x86_64 the lookup must resolve — a bundle missing the host's binary would otherwise + // fail an install after the overlay had already been deployed. On a host whose platform + // postdates the bundle (an arm64 box reading the first published one), it must fail with + // the reason, since every bundle is kept unchanged forever so `--bundle` stays + // reproducible and therefore cannot gain a key retroactively. + let bundle = parse(CURRENT).unwrap(); + match bundle.sidecar_asset() { + Ok(asset) => { + assert_eq!(asset.sha256.len(), 64); + assert!(asset.url.contains(&bundle.link.tag)); + } + Err(e) => { + let msg = e.to_string(); + assert!(msg.contains(platform_key().unwrap()), "{msg}"); + assert!(msg.contains("--bundle"), "{msg}"); + } + } + } + + #[test] + fn the_host_is_a_platform_the_components_are_built_for() { + // `cargo test` running at all means the host is one the crate compiles on, so a refusal + // here is a build target the release workflows have not caught up with. + let key = platform_key().unwrap(); + assert!( + ["linux-x86_64", "linux-aarch64", "windows-x86_64"].contains(&key), + "unexpected platform key {key}" + ); } #[test]