The sccache release asset is per-architecture and the Containerfile hardcoded the x86_64 triple, so an arm64 build produced an image whose sccache binary could not execute. Everything else in the image — the Debian base, the build dependencies, Node.js and rustup — already resolves per architecture, so that one URL was what pinned the image to amd64 and forced Rosetta or qemu on Apple Silicon. Resolve the triple from `uname -m`, which reflects the build platform under plain docker/podman build as well as buildx, unlike the BuildKit-only TARGETARCH. Document publishing `:latest` as a manifest list built natively on a host of each architecture, since a single-architecture tag sends the other side back to emulation, and note that Apple's `container` CLI needs Rosetta for its BuildKit helper VM regardless of the target. Pick the act_runner download by architecture for the same reason. Verified on arm64: the case arm selects sccache-v0.8.2-aarch64-unknown-linux-musl, and the installed binary reports `sccache 0.8.2` running natively. 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>
182 lines
7.2 KiB
Markdown
182 lines
7.2 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.
|
|
|
|
### 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`.
|