# Nix CI Docker images This directory builds the Docker images used by xrpld's Linux CI. Each image bundles the **exact same toolchain that the Nix development shell provides** (see [`docs/build/nix.md`](../../docs/build/nix.md)), so what runs in CI matches what developers get locally from `nix develop`. The toolchain (CMake, Ninja, Conan, GCC, Clang, clang-tidy, the sanitizer/coverage tools, …) is defined in [`nix/packages.nix`](../packages.nix) and assembled for CI by [`nix/ci-env.nix`](../ci-env.nix). The Docker build turns that Nix environment into an ordinary container image layered on top of a conventional base image (Ubuntu, Debian, RHEL, or `nixos/nix`). ## Images The images are built by the [`build-nix-images.yml`](../../.github/workflows/build-nix-images.yml) workflow and pushed to `ghcr.io/xrplf/xrpld/nix-`. The `` is selected through the `BASE_IMAGE` build argument; the base images are the **oldest supported version** of each distribution we target: | Image | `BASE_IMAGE` | Notes | | ------------ | -------------------------------------------- | -------------------------------------------------- | | `nix-nixos` | `nixos/nix:latest` | Build/lint only; binaries are not run (see below). | | `nix-ubuntu` | `ubuntu:20.04` | Oldest supported Ubuntu (glibc 2.31). | | `nix-debian` | `debian:bookworm` | | | `nix-rhel` | `registry.access.redhat.com/ubi9/ubi:latest` | | All images carry the full toolchain on `PATH` (via `/nix/ci-env/bin`) plus the CA bundle shipped in the Nix environment, so HTTPS clients (git, curl, Conan) work without `ca-certificates` being installed in the base image. ## Build stages [`Dockerfile`](./Dockerfile) is a multi-stage build: 1. **`builder`** — On a `nixos/nix` builder, evaluate the flake and build the CI environment (`nix/ci-env.nix`). The resulting Nix store closure (the complete set of store paths the toolchain depends on) is copied into a staging directory. 2. **`final`** — Start from `BASE_IMAGE`, copy in the Nix store closure and the `ci-env` symlink tree, and wire up `PATH` and the CA bundle. It then: - installs the dynamic linker if the base image lacks one (see [How libc is handled](#how-libc-is-handled)), - runs [`bin/check-tools.sh`](../../bin/check-tools.sh) to verify every expected tool is present and runnable. - compiles the C++ test programs in [`test_files/cpp/sources/`](./test_files/cpp/sources) with both `g++` and `clang++`, and sanitizers, and - compiles the Rust test programs in [`test_files/rust/sources/`](./test_files/rust/sources) with `rustc`, and builds the [`test_files/rust/proc_macro/`](./test_files/rust/proc_macro) workspace with `cargo` to exercise proc-macro dylib loading. 3. **`tester`** — Start again from a clean `BASE_IMAGE` (no Nix toolchain), install only the sanitizer runtime libraries ([`install-sanitizer-libs.sh`](./install-sanitizer-libs.sh)), and run the binaries compiled in `final`. This proves the binaries built with the Nix toolchain actually run on a vanilla base image. On `nixos/nix` this step is skipped (the binaries are patched for a conventional FHS loader). 4. **Output** — The final image is gated on the tester succeeding: it copies a sentinel file out of `tester`, so a failed test run fails the whole build. ## How libc is handled The goal is for binaries built in these images to run on the **oldest supported base image** (Ubuntu 20.04, glibc 2.31) and newer — without the developer's Nix toolchain being present at runtime. Two pieces make that work: - **Compilers linked against an old glibc.** The Nix CI environment does not use nixpkgs' current glibc. Instead it pins a 2020 nixpkgs snapshot whose primary glibc is **2.31** (matching Ubuntu 20.04), via the `nixpkgs-custom-glibc` flake input. GCC, Clang, binutils and compiler-rt are all rebuilt/wrapped against this custom glibc (see [`nix/ci-env.nix`](../ci-env.nix)). As a result the libraries they emit (`libstdc++`, `libgcc_s`, the sanitizer runtimes) reference only symbols available in glibc 2.31. - **An expected dynamic linker in the image.** Binaries built in Nix environments reference a dynamic linker from Nix store paths, which won't be present in the base image. However, [`loader-path.sh`](./loader-path.sh) reports the expected loader path for the current architecture, so we can patch the binaries to use the correct loader. The build then verifies all of this end to end, and the C++ and Rust programs go through the same pipeline: each is compiled in `final`, has its `PT_INTERP` patched to the target loader, and is then run in the clean `tester` stage to confirm it emits the expected diagnostic on a stock base image. The C++ programs are in `test_files/cpp/sources/` (a regular binary plus ASan/TSan/UBSan variants); the Rust programs are in `test_files/rust/sources/` (a hello binary plus panic and overflow-check variants), plus the `test_files/rust/proc_macro/` workspace — a crate whose compilation additionally loads a proc-macro dylib, and whose resulting binary is patched and run like the others. ## Files | File | Purpose | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. | | [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. | | [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. | | [`./test_files/rust/`](./test_files/rust) | Rust smoke test: rustc sources + a cargo proc-macro workspace + compile/run scripts. | | [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. | | [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |