anvilsign in

collin/anvil · 1404924c

Pin the Rust version in one place: rust-toolchain.toml

Collin Richards · 2026-08-25 04:03 UTC · 1404924cbb6e5c679ff4e2921c2f340e72a179d6 · parent f89a7ca7 · browse files

modified.githooks/pre-commit+16 −2
11 #!/bin/sh
2-# Pre-commit: rustfmt (nightly, for the unstable options in rustfmt.toml),
3-# cargo-sort-derives, and clippy.
2+# Pre-commit: the toolchain-pin check, rustfmt (nightly, for the unstable
3+# options in rustfmt.toml), cargo-sort-derives, and clippy.
44 # Wired up via `git config core.hooksPath .githooks`.
55 set -e
66
7+# rust-toolchain.toml is the one place the Rust version is set, but Cargo's
8+# `rust-version` floor is a separate key it cannot derive. Rather than let the
9+# two drift silently, check that the pin satisfies the floor.
10+PIN=$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' rust-toolchain.toml)
11+MSRV=$(sed -n 's/^[[:space:]]*rust-version[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' Cargo.toml)
12+case "$PIN" in
13+ "$MSRV" | "$MSRV".*) ;;
14+ *)
15+ echo "pre-commit: rust-toolchain.toml pins $PIN but Cargo.toml rust-version is $MSRV." >&2
16+ echo " Update the floor to match the pin." >&2
17+ exit 1
18+ ;;
19+esac
20+
721 if ! cargo +nightly fmt --all -- --check >/dev/null 2>&1; then
822 cargo +nightly fmt --all
923 echo "pre-commit: rustfmt reformatted files — review, re-stage, and commit again." >&2
⋯ 12 unchanged lines
modifiedCLAUDE.md+11 −3
⋯ 11 unchanged lines
1212 side). Tests may use the git CLI, but only as a fixture/interop check, never
1313 on the code path under test.
1414
15-Dev setup: `git config core.hooksPath .githooks` — the pre-commit hook runs
16-`cargo +nightly fmt` (nightly, for the unstable options in rustfmt.toml),
17-`cargo sort-derives` (`cargo install cargo-sort-derives`), and
15+Dev setup: `git config core.hooksPath .githooks` — the pre-commit hook checks
16+that `rust-toolchain.toml`'s pin satisfies Cargo.toml's `rust-version` floor,
17+then runs `cargo +nightly fmt` (nightly, for the unstable options in
18+rustfmt.toml), `cargo sort-derives` (`cargo install cargo-sort-derives`), and
1819 `cargo clippy --workspace --all-targets -- -D warnings`.
1920
21+**The Rust version is set in exactly one place: `rust-toolchain.toml`.** It
22+pins the toolchain and the musl cross target for every `cargo` invocation here,
23+and `deploy/runner/build.sh` parses `[toolchain] channel` out of it to bake the
24+same version into `anvil-runner:rust` — so a CI job and a checkout compile with
25+the same rustc. Bumping Rust means editing that file and Cargo.toml's floor
26+(the hook catches you if you forget the second), then rebuilding the image.
27+
2028 ## Viewing attachments referenced in tasks
2129
2230 TODO items and tickets may embed an uploaded image as
⋯ 106 unchanged lines
modifiedDEPLOY.md+9 −47
⋯ 2 unchanged lines
33 anvil runs as a single Docker container at `anvil.richardscollin.com`, fronted by
44 hagrid's Caddy reverse proxy.
55
6-- **Web** — anvil listens on `:3000` *inside* the container. Caddy (on the
7- `hagrid` Docker network) reverse-proxies to it and provides HTTPS via Let's
8- Encrypt. The web port is **not** published to the host.
9-- **SSH** — Caddy only fronts HTTP(S), so anvil's SSH server is **published
10- directly to the host**: `:2222` in the container, mapped to `:22` on the
11- droplet's default public IPv4 (§4). Git-over-SSH connects to
12- `git@anvil.richardscollin.com` on the default port.
13-- **Runtime** — fully self-contained: SQLite and the SSH crypto are compiled in,
14- and gix is pure-Rust. No git, OpenSSH, or system sqlite in the image.
15-
16-## 1. DNS
17-
18-Add an `A`/`AAAA` (or `CNAME` to hagrid) record:
19-
20-```
21-anvil.richardscollin.com -> <hagrid's public IP>
22-```
23-
24-This one record covers both the web (443, via Caddy) and SSH (22, direct to
25-the host). The `A` record must be the same address `compose.yaml` binds the SSH
26-port to — today `165.232.162.167` (§4).
27-
28-## 2. Caddy + index (in the hagrid repo)
29-
30-In `~/Code/hagrid/Caddyfile`, add:
31-
32-```
33-anvil.richardscollin.com {
34- reverse_proxy anvil:3000
35-}
36-```
37-
38-In `~/Code/hagrid/sites.yaml`, add an entry:
39-
40-```yaml
41-- name: anvil
42- host: anvil.richardscollin.com
43-```
44-
45-Then reload Caddy (`./hagrid.sh reload`, or `./hagrid.sh deploy` to push to the
46-host). Caddy resolves `anvil:3000` by container name over the `hagrid` network,
47-so the anvil container must join that network (`compose.yaml` does this).
48-
49-## 3. Deploying: `compose.yaml` + `hag`
6+## Deploying: `compose.yaml` + `hag`
507
518 `compose.yaml` at the repo root describes the deployment — the image, the
529 network, the volume, the published SSH port. Running it on hagrid is
5310 [`hag`](https://anvil.richardscollin.com/collin/hagrid), the shared deploy tool
5411 for every project on that host: it builds the image, pushes it to
5512 `registry.vibe.richardscollin.com`, copies `compose.yaml` up, then pulls and
56-recreates the container there. Install it once with `~/Code/hagrid/hag install`.
13+recreates the container there.
5714
5815 `compose.override.yaml` sits next to it for local development — loopback ports,
5916 the dev config, the Docker socket agent sessions need. Compose merges it
⋯ 26 unchanged lines
8643 ```sh
8744 dnf install zig # or: brew install zig
8845 cargo install cargo-zigbuild
89-rustup target add x86_64-unknown-linux-musl
9046 docker login registry.vibe.richardscollin.com # on this machine and hagrid
9147 ```
9248
49+The Rust toolchain itself needs no setup step: `rust-toolchain.toml` pins the
50+version *and* the `x86_64-unknown-linux-musl` target, and rustup installs both
51+on the first `cargo` invocation in the repo. That file is the one place the
52+Rust version is set — `deploy/runner/build.sh` reads it to bake the same
53+toolchain into `anvil-runner:rust`.
54+
9355 Every build is tagged twice: with the short git sha of the checkout (plus a
9456 `-dirty` suffix when the working tree has uncommitted changes) and with
9557 `latest`. The deploy pins the host to the exact sha it just pushed, so `hag
⋯ 150 unchanged lines
246208 plus steps:
247209
248210 ```yaml
249-image: rust:1.95-bookworm
211+image: anvil-runner:rust
250212 steps:
251213 - name: test
252214 run: cargo test --workspace
⋯ 64 unchanged lines
modifieddeploy/build.sh+2 −1
⋯ 17 unchanged lines
1818 # Prereqs (one-time):
1919 # zig (dnf install zig, or brew install zig)
2020 # cargo install cargo-zigbuild
21-# rustup target add x86_64-unknown-linux-musl
21+# The toolchain and the musl target come from rust-toolchain.toml — rustup
22+# installs both on the first cargo invocation in the repo.
2223 set -euo pipefail
2324
2425 TARGET="x86_64-unknown-linux-musl"
⋯ 33 unchanged lines
modifieddeploy/runner/Dockerfile+5 −2
⋯ 7 unchanged lines
88 # Parameterized so the same recipe produces a small general runner and a
99 # toolchain-carrying one:
1010 #
11-# anvil-runner:latest RUST_VERSION= (the default)
12-# anvil-runner:rust RUST_VERSION=1.98.0 (builds anvil itself)
11+# anvil-runner:latest RUST_VERSION= (the default)
12+# anvil-runner:rust RUST_VERSION=<channel> (builds anvil itself)
13+#
14+# build.sh reads `<channel>` out of the repo's rust-toolchain.toml, so the
15+# image's toolchain and the checkout's are the same by construction.
1316 #
1417 # Both sit on ubuntu:26.04. The official Rust image is Debian-based and has no
1518 # Ubuntu variant, so rather than let one tag drift onto a different distro the
⋯ 139 unchanged lines
modifieddeploy/runner/build.sh+11 −3
⋯ 22 unchanged lines
2323 NAME="${ANVIL_RUNNER_IMAGE:-anvil-runner}"
2424 CHANNEL="${CLAUDE_CHANNEL:-stable}"
2525
26-# Toolchain baked into the `rust` tag. Keep in step with the workspace
27-# `rust-version` in Cargo.toml.
28-RUST_VERSION="${RUST_VERSION:-1.98.0}"
26+# Toolchain baked into the `rust` tag, read from the repo's rust-toolchain.toml
27+# so the image compiles with the same rustc this checkout does. Not duplicated
28+# here on purpose: that file is the one place the version is set.
29+RUST_VERSION="${RUST_VERSION:-$(
30+ sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' \
31+ ../../rust-toolchain.toml
32+)}"
33+if [ -z "$RUST_VERSION" ]; then
34+ echo "could not read [toolchain] channel from rust-toolchain.toml" >&2
35+ exit 1
36+fi
2937
3038 build() {
3139 local tag="$1" rust="$2"
⋯ 30 unchanged lines
modifieddocs/agent-sessions.md+7 −1
⋯ 53 unchanged lines
5454 | Tag | Base | For |
5555 | -------------------- | ------------------------ | ------------------------------- |
5656 | `anvil-runner:latest`| `ubuntu:26.04` | the default, general work |
57-| `anvil-runner:rust` | `ubuntu:26.04` + rustup 1.98.0 | pipelines needing the toolchain |
57+| `anvil-runner:rust` | `ubuntu:26.04` + rustup | pipelines needing the toolchain |
58+
59+The version rustup installs is not written here or in the Dockerfile: the
60+build script reads `[toolchain] channel` from the repo's `rust-toolchain.toml`,
61+so a job compiles with the same rustc a checkout does. Bumping Rust is that one
62+file (plus the `rust-version` floor in `Cargo.toml`, which the pre-commit hook
63+checks agrees) and a rebuild of this tag.
5864
5965 **There is no registry behind this image.** It is built straight into the
6066 Docker daemon's local store, so it must be built on whichever host owns the
⋯ 90 unchanged lines
addedrust-toolchain.toml+18 −0
1+# The one place the Rust version is set. rustup reads this for every cargo
2+# invocation in the repo (installing the toolchain on first use), and
3+# deploy/runner/build.sh parses `channel` out of it to bake the same version
4+# into the anvil-runner:rust image — so the toolchain a CI job compiles with is
5+# the one this checkout compiles with.
6+#
7+# The `rust-version` floor in Cargo.toml has to agree; .githooks/pre-commit
8+# fails the commit if the two drift.
9+#
10+# `cargo +nightly fmt` (rustfmt.toml uses unstable options, see the hook) is
11+# unaffected: an explicit +toolchain overrides this file.
12+[toolchain]
13+channel = "1.98.0"
14+# deploy/build.sh cross-compiles the static binary the images COPY in. Listing
15+# the target here means rustup installs it, so there is no `rustup target add`
16+# step to remember.
17+targets = ["x86_64-unknown-linux-musl"]
18+components = ["clippy", "rustfmt"]