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/kernel-toolchain.Containerfile

# nordisk KERNEL TOOLCHAIN — the ONE image every step of the kernel kind runs in.
#
# The host contributes a container engine socket and a workdir. NOTHING else:
# no host gcc, no host python, no host lsmod, no host qemu. A build that only
# works on one box is a bug, not a build (workspace_nordisk/.nornir/
# kernel-trim-design.md §0, the host-free law).
#
# 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.
#
# ── THIS FILE IS REPRODUCIBLE, AND EVERY LINE THAT MAKES IT SO IS MEASURED ───
#
# MEASURED on oden 2026-08-30 (lane AV): six mints of the PREVIOUS version of
# this file into six fresh empty `--root` stores produced six different image
# ids (lane AT). Four mints of THIS one, spread over ~15 minutes, produced ONE:
#
#     image id        3f632569f1980e96744eef4ab258341fc77c55cb17c2dabe79b243e00d6e0825
#     manifest digest 78eaa55a651d5bd76aae36db0f3307e267a92d2a547ee338a17822a103e0320e
#
# So the pin is a property of THIS RECIPE, not of the box that ran it. Two axes
# had to be closed and BOTH are load-bearing — each was removed on its own and
# the two mints then disagreed (the ablations are in the gate's doc comment,
# `tests/toolchain_images_live.rs`):
#
#   the WORLD moves — `FROM debian:trixie` is a tag someone can move, and
#     `apt-get install` takes whatever the mirror says today. Closed by the
#     base image's DIGEST and by pointing apt at the SNAPSHOT the base image
#     itself records (`snapshot.debian.org/…/20260713T000000Z`, the timestamp
#     written as a comment in this very base image's own sources file), and by
#     pinning every pip requirement to an exact version.
#   the CLOCK moves — mtimes, PEP 552 `.pyc` headers, apt/dpkg logs, and the
#     random `/tmp/pip-install-XXXXXXXX/` directory pip compiles C extensions
#     in. Closed by `SOURCE_DATE_EPOCH` (CPython then emits hash-based pycs
#     instead of mtime-based ones), by deleting the logs and caches IN THE
#     LAYER THAT MAKES THEM, by building kmax's extension in a FIXED directory
#     with `-g0`, and — the one thing this file cannot say itself — by the
#     builder being handed the same instant for the image config and the layer
#     timestamps. Callers must pass it: `podman build --timestamp <epoch>`, or
#     `draupnir`'s `build_image_reproducible`, which sends podman's `timestamp`
#     build parameter. `KernelBuild::toolchain_source_date_epoch()` reads the
#     number out of the `ENV SOURCE_DATE_EPOCH` line BELOW, so there is one
#     writer of it and this file is that writer.
#
# ⚠ WHAT THIS COSTS: minting now depends on snapshot.debian.org being up. It is
# a Debian-project service with no uptime promise. If it goes away the mint
# fails LOUDLY (apt cannot reach its mirror) rather than silently drifting, the
# already-minted image is unaffected, and the recipe still names the exact
# instant — any mirror of that snapshot restores it. That trade is deliberate:
# a build that stops is recoverable, a pin that quietly stops meaning anything
# is not.
#
# Build it through the image kind, then PIN IT BY DIGEST in the declaration
# (`toolchain_image_sha256`) and leave `toolchain_packages = []` — the image is
# self-contained, so the build runs `--network none` and cannot drift under you.

# The debian:trixie manifest-list digest as of 2026-08-30. `debian:trixie` is a
# TAG: lane AT measured the base layer of a 08-27 mint (102f96b3f4ce…) against a
# 08-30 mint of the same tag (31631a95db7c…) — the same two words, two different
# base images. This digest resolves to 102f96b3f4ce… and will keep doing so.
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.
ENV SOURCE_DATE_EPOCH=1783900800

# step: compile. The kernel's own minimum — gcc + make + the kconfig language
# tools; libelf for pahole/BTF, openssl for module/cert paths.
# step: LTO/DCE (design §3, third knife). CONFIG_LTO_CLANG_FULL builds with
#   LLVM=1, which needs clang AND lld — GCC-LTO is not mainline.
# step: MINIFY (design §3, second knife). klocalizer/kclause are kmax,
#   python+z3; they run HERE, never on the host. python3-dev is NOT optional:
#   kmax ships kextractor, the kernel's OWN kconfig parser as a C extension, and
#   builds it from source at install time. MEASURED 2026-08-22 — without it the
#   image build dies on "kextractor_extension.c:1:10: fatal error: Python.h: No
#   such file".
# step: PROBE-BOOT (design §4). The candidate bzImage is booted in QEMU INSIDE
#   this container and reports over serial — this is what replaces
#   localmodconfig's "boot the target and read lsmod on the host".
# step: probe-boot's initramfs + any medium mastering the probe needs (cpio,
#   xorriso).
# step: fetch/verify are HOST-side (pure Rust), but a container that can unpack
#   what it was handed keeps the script honest (tar).
#
# The three `rm` lines at the end are not tidiness. MEASURED: without them two
# mints of this layer differ in exactly five files — /var/log/apt/{history,term,
# eipp}.log*, /var/log/dpkg.log, /var/log/alternatives.log and ldconfig's
# aux-cache — and five files are enough to move the layer digest and therefore
# the image id. They are deleted in the SAME layer that creates them, because a
# later `rm` leaves the bytes in the layer below.
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 \
      gcc make flex bison bc libelf-dev libssl-dev xz-utils dwarves \
      clang lld llvm \
      python3 python3-dev python3-pip python3-venv \
      qemu-system-x86 \
      cpio xorriso \
      tar; \
    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

# kmax carries klocalizer + kclause (the SAT minifier over the Kconfig DAG).
# Installed into a venv: Debian's PEP-668 marks the system python EXTERNALLY-
# MANAGED, and pip's break-the-system escape hatch is exactly the kind of
# "works on my image" shortcut that makes a toolchain unreproducible. (The test
# asserts that flag's literal spelling appears NOWHERE in this file.)
#
# Every version below is EXACT. `pip install kmax` resolves against PyPI's
# today, which is the same defect `FROM debian:trixie` is. PyPI never rewrites a
# released file, so an exact version is a content pin.
#
# kmax is the only requirement with no wheel: it compiles `kextractor` (the
# kernel's own kconfig parser) from C at install time. Lane AT measured the
# eight resulting `.so` files differing in 132 of 426 528 bytes between two
# mints — a 20-byte GNU build-id and 102 bytes of `.debug_str` holding pip's
# random `/tmp/pip-install-XXXXXXXX/kmax_<32 hex>` build directory. So the sdist
# is unpacked to a FIXED path and built there (pip builds a local directory
# in-tree), with `-g0` so there is no `.debug_str` to hold a path at all, and
# `--no-build-isolation` so the build backend is the pinned setuptools in this
# venv rather than whatever PyPI serves as "latest" that minute.
RUN set -eu; \
    python3 -m venv /opt/kmax; \
    /opt/kmax/bin/pip install --no-cache-dir setuptools==84.0.0 wheel==0.45.1; \
    /opt/kmax/bin/pip install --no-cache-dir \
      astutils==0.0.6 certifi==2026.7.22 charset-normalizer==3.5.1 dd==0.6.0 \
      enum34==1.1.10 idna==3.19 networkx==3.6.1 packaging==26.3 ply==3.10 \
      regex==2026.7.19 requests==2.34.2 tqdm==4.70.0 urllib3==2.7.0 \
      whatthepatch==1.0.7 z3-solver==5.1.0.0; \
    mkdir -p /build; \
    /opt/kmax/bin/pip download --no-cache-dir --no-binary :all: --no-deps -d /build kmax==4.10; \
    tar -C /build -xf /build/kmax-4.10.tar.gz; \
    CFLAGS="-g0" /opt/kmax/bin/pip install --no-cache-dir --no-deps --no-build-isolation /build/kmax-4.10; \
    rm -rf /build; \
    rm -rf /root/.cache
ENV PATH="/opt/kmax/bin:${PATH}"

# The build script runs as `sh -eu -c` with the workdir bind-mounted at /work.
WORKDIR /work