crates/nornir-build-thing/src/containerfiles/cargo-toolchain.Containerfile
# nordisk CARGO TOOLCHAIN — the ONE image every cargo variant compiles in.
#
# The host contributes a container engine socket and a workdir. NOTHING else:
# no host rustup, no host musl-gcc, no host mold, no host ~/.cargo/config.toml.
# A build that only works on one box is a bug, not a build (the same host-free
# law kernel-toolchain.Containerfile states, applied to the kind that had no
# image at all).
#
# WHY THIS FILE EXISTS — the measurement it closes (2026-08-25):
# `dwarves buildthing gunnar --variant gunnar-server` FAILED in 1.8 s on oden
# with `error[E0463]: can't find crate for core`, because every gunnar variant
# targets `x86_64-unknown-linux-musl` and oden's rustup had no musl std. The
# same failure reproduces on t14s by naming a toolchain that lacks the target
# (`RUSTUP_TOOLCHAIN=1.96.0 cargo build --target x86_64-unknown-linux-musl` →
# `error[E0463]: can't find crate for std`). The build's outcome was decided by
# which box it landed on. That is the defect; this image is the fix.
# And gunnar's own declaration said so out loud —
# `gunnar/.nornir/build-variants.toml:378`: "⚠ THIS NEEDS A musl C TOOLCHAIN ON
# THE BUILD HOST, and it is not optional." It is now optional on the host,
# because it is mandatory in here.
#
# Every package below names the STEP that needs it. Nothing rides "just in case"
# — an unexplained package in a build image is an unmeasured dependency.
#
# Build it through `CargoToolchain::build_image`, then PIN IT BY DIGEST in the
# declaration (`toolchain_image_sha256`) — a named tag is a convenience, not a
# pin. The image is self-contained, so the compile runs `--network none`.
#
# ── REPRODUCIBLE, the same four moves kernel-toolchain.Containerfile makes ────
#
# MEASURED 2026-09-05 (lane bevis): this file had NONE of them while its sibling
# had all four, and `DraupnirCargoRunner::build_toolchain_image` called the plain
# `build_image` where the kernel runner calls `build_image_reproducible`. So the
# kernel toolchain re-minted to the same id and the cargo toolchain re-minted to a
# NEW id every time — gunnar moved its `[build.toolchain] image_sha256` pin twice
# in one day for exactly that reason. A pin on such an image names a minting
# event, not a recipe. The four moves, each closing one thing that moves:
# the BASE moves — `FROM debian:trixie` is a tag someone can move. Closed by
# the digest below, the SAME digest the kernel file pins so
# the constellation has one base, not two.
# the ARCHIVE moves — `apt-get install` takes whatever the mirror says today.
# Closed by pointing apt at the snapshot.debian.org instant
# this base image was itself built from.
# the CLOCK moves — mtimes, apt/dpkg logs, ldconfig's aux-cache. Closed by
# `SOURCE_DATE_EPOCH` and by deleting the logs IN THE LAYER
# THAT MAKES THEM, and by the builder being handed the same
# instant for the image config (`build_image_reproducible`).
# the TOOLCHAIN moves — closed already: `RUST_VERSION` names an exact release
# and static.rust-lang.org's per-version artifacts are
# immutable. rustup verifies them by sha256 against its
# manifest, which is the pin this file inherits rather than
# restates. What is NOT closed: that fetch still leaves the
# snapshot; a mint needs static.rust-lang.org reachable.
#
# The two Containerfiles state these numbers separately because an OCI build has
# no include. `cargo_container::tests::the_two_toolchains_pin_one_world` asserts
# they AGREE — base digest, snapshot instant, epoch — so the duplication cannot
# drift silently.
# The debian:trixie manifest-list digest as of 2026-08-30 — the one the kernel
# toolchain pins (kernel-toolchain.Containerfile), so both images share a base.
FROM debian:trixie@sha256:d63a99144861e4e460196ed93d07777490cbeab53ca660c434f2a589a6c50ea3
ENV DEBIAN_FRONTEND=noninteractive
# 2026-07-13T00:00:00Z — the Debian snapshot this base image was built from, so
# the image's clock and the archive it installs from name the same instant. The
# ONE writer of this number: the builder reads it from here
# (`cargo_container::cargo_toolchain_source_date_epoch`).
ENV SOURCE_DATE_EPOCH=1783900800
# The rust toolchain version this image pins. Passed as a build ARG so the image
# is built for the EXACT rustc a repo compiles with: two images differing only in
# this string are two different toolchains and must not share a tag.
ARG RUST_VERSION=1.97.1
# The target triples `rustup target add`s. Space-separated. The host's installed
# target set stops being an input the moment these are baked in.
#
# Each triple names the ROAD that needs it, the same way every apt package below
# names its step — a target riding "just in case" is 40-90 MB of unmeasured std:
# x86_64-unknown-linux-gnu road: HOST BINARIES. The default for every native
# tool and for the byte-identity gate's host side.
# x86_64-unknown-linux-musl road: APPLIANCE. Every gunnar variant targets it
# (build-variants.toml:378) and it is what the
# `E0463 can't find crate for core` at the top of
# this file was missing.
# wasm32-unknown-unknown road: WASM. MEASURED on oden 2026-08-27: the
# chained build refused `gunnar-build-thing-wasm`
# BY NAME — "targets wasm32-unknown-unknown, which
# the toolchain image nordisk-cargo-toolchain does
# not carry std for (it declares:
# x86_64-unknown-linux-gnu,
# x86_64-unknown-linux-musl)". The refusal was
# correct and it named this fix. Without it the
# same E0463 lands inside the container instead:
# `rustc --target wasm32-unknown-unknown` in the
# 035814c4 image says "can't find crate for std …
# consider downloading the target with rustup
# target add wasm32-unknown-unknown".
# PROVEN on the image this file now builds
# (sha256:07b4e3a8…), oden 2026-08-27: the REAL
# plugin crate compiles inside it with the network
# off and the registry read-only —
# `cargo build --offline --locked -p
# gunnar-build-thing --target
# wasm32-unknown-unknown` finished in 12.48 s and
# emitted a 55 079 991-byte cdylib opening with
# 00 61 73 6d.
# x86_64-unknown-uefi (2026-09-15, lane I): korp-installer's compose builds
# korp-boot (EFI/BOOT/BOOTX64.EFI) for it — the
# third of the three compose builds that ran HOST
# cargo before they moved into this image.
ARG RUST_TARGETS="x86_64-unknown-linux-gnu x86_64-unknown-linux-musl wasm32-unknown-unknown x86_64-unknown-uefi"
# apt reads the SNAPSHOT this base image was built from — the same instant
# `SOURCE_DATE_EPOCH` names above — so "whatever the mirror says today" stops
# being an input. Check-Valid-Until is off because a snapshot's Release file is,
# by construction, older than apt's default validity window.
RUN set -eu; \
printf '%s\n' \
'Types: deb' \
'URIs: http://snapshot.debian.org/archive/debian/20260713T000000Z' \
'Suites: trixie trixie-updates' \
'Components: main' \
'Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp' \
'' \
'Types: deb' \
'URIs: http://snapshot.debian.org/archive/debian-security/20260713T000000Z' \
'Suites: trixie-security' \
'Components: main' \
'Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp' \
> /etc/apt/sources.list.d/debian.sources; \
printf 'Acquire::Check-Valid-Until "false";\n' > /etc/apt/apt.conf.d/10no-check-valid-until; \
apt-get update && apt-get install -y --no-install-recommends \
`# step: COMPILE — rustup itself. Debian trixie packages it (1.27.1-3+b1),` \
`# so the toolchain arrives through the distro's signed archive instead of` \
`# a curl|sh that no digest covers. rustup then fetches the PINNED rustc.` \
rustup \
`# step: COMPILE — rustup's downloader and the archive's trust root.` \
`# ca-certificates is what makes the static.rust-lang.org fetch verifiable;` \
`# without it rustup fails on TLS and the image build is a silent tag.` \
ca-certificates \
`# step: LINK (glibc targets) — rustc drives the C compiler as its linker` \
`# driver, so cc + binutils are not optional for x86_64-unknown-linux-gnu.` \
gcc libc6-dev \
`# step: LINK (musl targets) — musl-gcc + musl's libc.a. MEASURED on oden` \
`# 2026-08-03: without these, any crate with a cc-rs build script dies on` \
`# "failed to find tool x86_64-linux-musl-gcc", and gunnar's declaration` \
`# names that exact error at build-variants.toml:381.` \
musl-tools musl-dev \
`# step: LINK — the linker rustc actually invokes for musl static-PIE, and` \
`# the one that does not need a GNU ld built for a second triple. mold is` \
`# deliberately ABSENT: the host's ~/.cargo/config.toml adds` \
`# -C link-arg=-fuse-ld=mold for the gnu target, and a host linker choice` \
`# leaking into a container build is exactly the drift this image removes.` \
lld \
`# step: FETCH — cargo resolves git dependencies by running git. A repo` \
`# with one dependency declared as a git URL would otherwise fail inside` \
`# the image and succeed on the host: the same one-box bug, smaller.` \
git \
`# step: PROVENANCE — the produced binary is content-hashed and its dynamic` \
`# section read to prove a musl artifact is actually static. binutils is` \
`# what linkage's readelf-shaped checks need on the container side.` \
binutils \
`# step: CODEGEN — tonic-build SHELLS OUT to protoc and does not ship one.` \
`# MEASURED 2026-08-26: gunnar-server got 178 s into a container build with` \
`# every path dependency resolved and died in gunnar-proto/build.rs:40 on` \
`# "no protoc on PATH and $PROTOC is unset". Every gRPC service in the` \
`# constellation has that build script, so this is not a gunnar package.` \
protobuf-compiler \
`# step: LINK — cc-rs and pkg-config-rs locate system libraries through` \
`# pkg-config; without it a -sys crate reports a library ABSENT that is` \
`# installed, which reads as a missing dependency and is not one.` \
pkg-config \
`# step: COMPILE (C for wasm32) — cc-rs compiles a -sys crate's C for` \
`# wasm32-unknown-unknown with clang (gcc has no wasm backend) and archives` \
`# it with llvm-ar (binutils ar writes no symbol index for wasm objects).` \
`# MEASURED on dwarf 2026-09-29: facett-demo-wasm died in zstd-sys (parquet` \
`# zstd, the browser demo's embedded GeoParquet) on "failed to find tool` \
`# clang"; in this image + clang llvm it builds. Versions are the snapshot's.` \
clang llvm \
`# step: LINK — nornir's code room (graphblas-sys) links -lgraphblas and` \
`# reads GraphBLAS.h. MEASURED on loki 2026-10-16: nornir-solo and` \
`# nornir-server failed to link: cannot find -lgraphblas, in this image.` \
libgraphblas-dev \
`# The clock's fingerprints, deleted IN THE LAYER THAT MAKES THEM: apt's` \
`# lists, the apt/dpkg/alternatives logs and ldconfig's aux-cache each` \
`# carry a wall-clock stamp, and a later rm leaves the bytes in the layer` \
`# below. Five such files are enough to move the image id.` \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /var/log/apt /var/log/dpkg.log /var/log/alternatives.log \
&& rm -f /var/cache/ldconfig/aux-cache \
&& rm -rf /root/.cache
# rustup and cargo live under /usr/local, NOT under a user's $HOME: the image
# must behave identically for whatever uid the engine runs it as, and a home
# directory is per-run state.
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
# The pinned toolchain and its targets. `--profile minimal` is deliberate: rustfmt,
# clippy and the docs are DEVELOPER tools, and a build image that carries them is
# carrying an unmeasured dependency (and ~400 MB) for a step that never runs them.
# A repo whose forge runs clippy declares it and gets its own image.
RUN rustup toolchain install "${RUST_VERSION}" --profile minimal --no-self-update \
&& rustup default "${RUST_VERSION}" \
&& for t in ${RUST_TARGETS}; do rustup target add --toolchain "${RUST_VERSION}" "$t"; done \
&& rustc --version
# ── THE IMAGE ASSERTS ITS OWN CONTENTS ───────────────────────────────────────
#
# What stood here was `rustc --print target-list > /dev/null`, and it was a
# HOLLOW assertion: target-list prints every triple rustc was COMPILED TO KNOW.
# MEASURED in the 035814c4 image on oden 2026-08-27: 322 lines, and
# `wasm32-unknown-unknown` is one of them — while its own
# `rustc --print target-libdir --target wasm32-unknown-unknown` points at a
# directory that DOES NOT EXIST. So the old check printed the target it had no
# std for and exited 0. A check that cannot fail is indistinguishable from a law
# being obeyed.
#
# So the image compiles, once per declared triple, a crate that needs rust-std
# (String is std, not core) and asserts the OUTPUT EXISTS AND IS NON-EMPTY. A
# missing std then fails HERE, in the image build, with the triple in the
# message — not an hour into a repo's compile with a bare E0463. The libdir is
# checked too, because that is the directory rustup target add actually creates.
RUN set -eu; \
printf 'pub fn probe() -> String { String::from("std") }\n' > /tmp/std-probe.rs; \
for t in ${RUST_TARGETS}; do \
libdir="$(rustc --print target-libdir --target "$t")"; \
test -d "$libdir" || { echo "FATAL: no rust-std libdir for $t ($libdir) — rustup target add did not take"; exit 1; }; \
rustc --edition 2021 --crate-type=lib --target "$t" \
-o "/tmp/std-probe-$t.rlib" /tmp/std-probe.rs \
|| { echo "FATAL: this image cannot compile for $t — rust-std is MISSING (add it to RUST_TARGETS)"; exit 1; }; \
test -s "/tmp/std-probe-$t.rlib" || { echo "FATAL: $t compiled to an EMPTY artifact"; exit 1; }; \
echo "std OK: $t -> $libdir"; \
done; \
rm -f /tmp/std-probe.rs /tmp/std-probe-*.rlib
# cc-rs looks up the C compiler for a target by the TRIPLE-PREFIXED name
# (`x86_64-linux-musl-gcc`). Debian's musl-tools installs the driver as plain
# `musl-gcc` only, so without these two links every cc-rs build script fails to
# find a tool that IS installed. Linked rather than exported as CC_* so the name
# resolves for anything that shells the compiler, not just for cargo's env view.
RUN ln -s /usr/bin/musl-gcc /usr/local/bin/x86_64-linux-musl-gcc \
&& ln -s /usr/bin/musl-gcc /usr/local/bin/x86_64-unknown-linux-musl-gcc
# ── THE HONEST GAP, stated rather than papered over ──────────────────────────
#
# There is NO C++ cross toolchain for musl in here. Debian packages a musl SHIM
# (musl-gcc), not a musl-cross: there is no `x86_64-linux-musl-g++` and no musl
# libunwind, so a crate whose build script compiles C++ for musl — MEASURED:
# openzl-sys-rs, which dies on `cannot find -lunwind` — still cannot build, in
# this image or on any Debian host. The constellation's measured answer is zig
# 0.16.0 behind cc/cmake-derived wrapper names (workspace_nordisk resume,
# `musl-FÄLLD`), and zig is NOT in trixie — it has to be fetched host-side,
# sha256-verified in Rust, and staged into the build context the way the kernel
# kind fetches its tarball. That is a declared next slice, not a silent absence:
# a repo needing it will fail here BY NAME rather than quietly link against the
# host's.
# The build script runs as `sh -eu -c` with the repo bind-mounted at /work.
WORKDIR /work