The workflow references the SDK image by the moving `:latest` tag, and act_runner skips the pull when a local copy of that tag already exists: the job log reports `docker pull ... forcePull=false` followed by `Image exists? true`. Pushing a rebuilt image therefore changes nothing until someone pulls on the VM by hand, and the run fails as though the image never gained the tool that was added to the Containerfile — `sccache` resolving as "No such file or directory" while the pinned toolchain from an earlier build of the same tag resolves fine. Set `force_pull: true` so a pushed image is what actually runs, and document the manual refresh for runners configured before this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space>
194 lines
7.8 KiB
Markdown
194 lines
7.8 KiB
Markdown
<!--
|
|
SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
-->
|
|
|
|
# trx-rs SDK image
|
|
|
|
A single container image that is the canonical build environment for trx-rs,
|
|
used **both** by CI and by developers. It bakes in the pinned Rust toolchain
|
|
(matching `rust-toolchain.toml`) and every build dependency, so the compiler
|
|
and `clippy` are identical everywhere — no "works on my machine".
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `Containerfile` | The SDK image (Debian + build deps + pinned Rust + Node + git). |
|
|
| `runner-config.example.yaml` | Example act_runner config for the CI VM (Docker executor). |
|
|
|
|
## Build and publish
|
|
|
|
Nothing in the image is architecture-specific: the base image, the Debian build
|
|
dependencies, Node.js, `rustup` and the `sccache` release all resolve per
|
|
architecture, so the same `Containerfile` builds natively on x86_64 and arm64.
|
|
|
|
Single architecture — the tag then only works on the architecture you built it
|
|
on:
|
|
|
|
```bash
|
|
# from the repo root
|
|
podman build -t git.haxx.space/sjg/trx-rs/sdk:latest container
|
|
podman login git.haxx.space
|
|
podman push git.haxx.space/sjg/trx-rs/sdk:latest
|
|
```
|
|
|
|
**Both architectures without emulation.** The CI runner is x86_64 and Apple
|
|
Silicon developer machines are arm64, so `:latest` has to be a manifest list —
|
|
a single-architecture tag makes the other side fall back to Rosetta or qemu.
|
|
Build each half natively on a host of that architecture, then join them:
|
|
|
|
```bash
|
|
# on an x86_64 host
|
|
podman build --platform linux/amd64 -t git.haxx.space/sjg/trx-rs/sdk:latest-amd64 container
|
|
podman push git.haxx.space/sjg/trx-rs/sdk:latest-amd64
|
|
|
|
# on an arm64 host
|
|
podman build --platform linux/arm64 -t git.haxx.space/sjg/trx-rs/sdk:latest-arm64 container
|
|
podman push git.haxx.space/sjg/trx-rs/sdk:latest-arm64
|
|
|
|
# from either, once both are pushed
|
|
podman manifest create git.haxx.space/sjg/trx-rs/sdk:latest \
|
|
git.haxx.space/sjg/trx-rs/sdk:latest-amd64 \
|
|
git.haxx.space/sjg/trx-rs/sdk:latest-arm64
|
|
podman manifest push --all git.haxx.space/sjg/trx-rs/sdk:latest
|
|
```
|
|
|
|
Building both from one machine is a single command
|
|
(`podman build --platform linux/amd64,linux/arm64 --manifest ...`), but the
|
|
foreign half runs under emulation and is slow — the two-host flow above is
|
|
what keeps every build native.
|
|
|
|
Tag with the Rust version too (e.g. `:1.97.1`) if you want reproducible pins.
|
|
Make the package **public** (Gitea → Packages → the image → Settings) so the CI
|
|
runner and developers can pull it without credentials. If you keep it private,
|
|
add `credentials:` under the workflow's `container:` and log the runner into the
|
|
registry.
|
|
|
|
Pushing a rebuilt image is not enough on its own: `:latest` is a moving tag, and
|
|
act_runner reuses whatever it cached the first time unless `force_pull: true` is
|
|
set (see `runner-config.example.yaml`). Without it the job log says
|
|
`Image exists? true` and the run behaves as though the image were never
|
|
rebuilt — a tool added to the `Containerfile` reads as missing from the image.
|
|
Either set `force_pull`, or refresh the VM's copy by hand:
|
|
|
|
```bash
|
|
docker pull git.haxx.space/sjg/trx-rs/sdk:latest
|
|
docker run --rm git.haxx.space/sjg/trx-rs/sdk:latest sccache --version
|
|
```
|
|
|
|
### macOS note
|
|
|
|
Apple's `container` CLI builds through a BuildKit helper VM that is configured
|
|
with Rosetta whether or not the target is x86_64, so `container build` fails
|
|
with *"Rosetta is not installed"* on a clean machine. That is a property of the
|
|
builder, not of this image — `container run` works natively without it. Either
|
|
install Rosetta once (`softwareupdate --install-rosetta`, after which an arm64
|
|
build still produces a native arm64 image), or build with Podman, whose arm64
|
|
BuildKit needs no emulation.
|
|
|
|
## Developer use
|
|
|
|
Reproducible one-off build, no local toolchain needed:
|
|
|
|
```bash
|
|
podman run --rm -it -v "$PWD":/work -w /work \
|
|
git.haxx.space/sjg/trx-rs/sdk:latest \
|
|
cargo build --release
|
|
```
|
|
|
|
Or open the repo in the image via VS Code / JetBrains "Reopen in Container"
|
|
(`.devcontainer/devcontainer.json` points at the same image).
|
|
|
|
Building outside the container? `rust-toolchain.toml` pins the same rustc, so
|
|
`rustup` installs the matching toolchain automatically.
|
|
|
|
## CI use
|
|
|
|
`.gitea/workflows/ci.yml` runs the `lint`, `test` and `frontend` jobs *inside*
|
|
this image via the `container:` key, so they skip all setup and go straight to
|
|
`cargo` and `npm`. The frontend job needs three things from the image beyond
|
|
Rust: Node.js for the toolchain, Chromium at `/usr/bin/chromium` for the
|
|
browser smoke test, and `cargo` — `npm run verify-generated` regenerates the
|
|
Rust wire contracts before checking for drift.
|
|
|
|
The `reuse` job stays on the upstream `fsfe/reuse-action` (a Docker action the
|
|
Docker executor launches as a sibling container) — nothing REUSE-related is
|
|
baked into the SDK, and it lints the whole repository, so no job runs its own
|
|
licence check.
|
|
|
|
## Compilation cache (sccache)
|
|
|
|
The SDK image ships [`sccache`](https://github.com/mozilla/sccache). It is
|
|
enabled via `RUSTC_WRAPPER=sccache` in CI and the devcontainer (not repo-wide,
|
|
so plain `cargo` builds outside the SDK are unaffected).
|
|
|
|
- **CI** persists the cache on the runner host — create the dir once:
|
|
`mkdir -p /var/cache/sccache`. It is bind-mounted into each job container at
|
|
`/sccache` (see `runner-config.example.yaml`), so cache survives across runs
|
|
and is shared between the lint/test jobs and both projects.
|
|
- **Devcontainer** uses a named volume (`trx-rs-sccache`).
|
|
- Check effectiveness with `sccache --show-stats` (the CI jobs print it).
|
|
|
|
`CARGO_INCREMENTAL=0` is set wherever sccache is on, since sccache cannot cache
|
|
incremental artifacts.
|
|
|
|
## CI runner (Alpine / OpenRC)
|
|
|
|
The runner uses the **Docker executor** (not the host executor): per-job
|
|
container isolation and standard `ubuntu-latest` semantics. `act_runner` runs
|
|
as an OpenRC service. Files provided:
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `act_runner.openrc` | OpenRC init script (`supervise-daemon`, depends on docker). |
|
|
| `act_runner.confd.example` | Per-instance `conf.d` settings for multi-runner hosts. |
|
|
|
|
**Cap the thread budget.** In a VM, pin its vCPUs to specific host threads
|
|
(libvirt/KVM):
|
|
|
|
```xml
|
|
<vcpu placement='static'>2</vcpu>
|
|
<cputune>
|
|
<vcpupin vcpu='0' cpuset='4'/>
|
|
<vcpupin vcpu='1' cpuset='5'/>
|
|
</cputune>
|
|
```
|
|
|
|
On bare metal, the `container.options: "--cpus=2"` and `capacity: 1` in
|
|
`runner-config.example.yaml` already bound each runner.
|
|
|
|
**Set it up:**
|
|
|
|
```bash
|
|
# 1. Docker + a dedicated user with socket access
|
|
apk add docker docker-cli
|
|
rc-update add docker default && rc-service docker start
|
|
adduser -S -D -H -h /var/lib/act_runner act
|
|
addgroup act docker
|
|
|
|
# 2. act_runner binary (static Go build, works on musl)
|
|
# Upstream publishes per-architecture builds; pick the host's.
|
|
case "$(uname -m)" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac
|
|
curl -fsSL -o /usr/local/bin/act_runner \
|
|
"https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-${arch}"
|
|
chmod +x /usr/local/bin/act_runner
|
|
|
|
# 3. Config + register one runner per project (scope keeps their jobs apart)
|
|
install -Dm644 container/runner-config.example.yaml /etc/act_runner/trx-rs.yaml
|
|
install -d -o act /var/lib/act_runner/trx-rs
|
|
su act -s /bin/sh -c 'cd /var/lib/act_runner/trx-rs && \
|
|
act_runner register --no-interactive \
|
|
--instance https://git.haxx.space --token <TOKEN> \
|
|
--name trx-rs-ci \
|
|
--labels "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"'
|
|
|
|
# 4. OpenRC service (repeat the symlink+conf.d for the second project)
|
|
install -m755 container/act_runner.openrc /etc/init.d/act_runner
|
|
ln -s act_runner /etc/init.d/act_runner.trx-rs
|
|
install -m644 container/act_runner.confd.example /etc/conf.d/act_runner.trx-rs
|
|
rc-update add act_runner.trx-rs default
|
|
rc-service act_runner.trx-rs start
|
|
```
|
|
|
|
Check it with `rc-service act_runner.trx-rs status` and
|
|
`tail -f /var/log/act_runner.trx-rs.log`.
|