Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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