# SPDX-License-Identifier: Apache-2.0 # Copyright Open Network Fabric Authors { platform ? "x86-64-v3", libc ? "gnu", profile ? "debug", instrumentation ? "none", sanitize ? "", features ? "", default-features ? "true", kernel ? "linux", tag ? "dev", nightly ? "false", }: let sources = import ./npins; # helper method to work around nix's contrived builtin string split function. split-str = split-on: string: if string == "" then [ ] else builtins.filter (elm: builtins.isString elm) (builtins.split split-on string); lib = (import sources.nixpkgs { }).lib; host-arch = (import sources.nixpkgs { }).stdenv.hostPlatform.parsed.cpu.name; platform' = import ./nix/platforms.nix { inherit lib platform libc kernel ; }; as-set = str: lib.sort (a: b: a < b) (lib.unique (split-str ",+" str)); sanitizers = as-set sanitize; instrumentations = as-set instrumentation; cargo-features = split-str ",+" features; profile' = import ./nix/profiles.nix { inherit sanitizers instrumentations profile cargo-features host-arch ; inherit (platform') arch target-cpu; }; # The same flag table with the sanitizer left out, which is how # `sanitizer-rustflags` below works out what the sanitizer added. Cheap: # `profiles.nix` is a pure attrset of flag lists and builds nothing. profile-unsanitized' = import ./nix/profiles.nix { inherit instrumentations profile cargo-features host-arch ; sanitizers = [ ]; inherit (platform') arch target-cpu; }; # Exactly the `RUSTFLAGS` the selected sanitizers contribute, by difference. # # Subtraction rather than a substring test because there is no substring that # catches them all: `-Clink-arg=-static-libasan` shares none with # `-Zsanitizer=address` or `-Zexternal-clangrt`, and each sanitizer spells its # runtime differently. Doing it by difference also means a flag added to # `profiles.nix` later is handled without anyone remembering this exists. sanitizer-rustflags = lib.subtractLists profile-unsanitized'.RUSTFLAGS profile'.RUSTFLAGS; # Test archives run on the host (e.g. `cargo nextest run --archive-file`) # rather than in the nix build sandbox, so panics in fixtures must # unwind for cleanup (netns / caps) to run. See `test-utils/src/lib.rs`. profile-tests' = import ./nix/profiles.nix { inherit sanitizers instrumentations profile cargo-features host-arch ; inherit (platform') arch target-cpu; for-tests = true; }; cargo-profile = { "debug" = "dev"; "release" = "release"; "checked" = "checked"; } .${profile}; overlays = import ./nix/overlays { inherit instrumentation libc nightly sanitizers instrumentations sources ; profile = profile'; platform = platform'; }; native-pkgs = import sources.nixpkgs { overlays = [ overlays.rust overlays.llvm overlays.dataplane overlays.dataplane-dev overlays.frr ]; }; pkgs = if platform != "wasm32-wasip1" then native-pkgs.pkgsCross.${platform'.info.nixarch} else native-pkgs; sysroot-stamp = '' printf '%s' '${builtins.concatStringsSep "," sanitizers}' > "$out/.sanitize" printf '%s' '${builtins.concatStringsSep "," instrumentations}' > "$out/.instrumentation" ''; sysroot = if platform != "wasm32-wasip1" then pkgs.symlinkJoin { name = "sysroot"; postBuild = sysroot-stamp; paths = with pkgs.pkgsHostHost; [ pkgs.pkgsHostHost.libc.dev # fully qualified: bare `libc` resolves to the "gnu" function argument, not pkgs.pkgsHostHost.libc pkgs.pkgsHostHost.libc.out # (same as above) fancy.dpdk-wrapper.dev fancy.dpdk-wrapper.out fancy.dpdk.dev fancy.dpdk.static fancy.hwloc.dev fancy.hwloc.static fancy.libbsd.dev fancy.libbsd.static fancy.libmd.dev fancy.libmd.static fancy.libnl.dev fancy.libnl.static fancy.libunwind.out fancy.numactl.dev fancy.numactl.static fancy.rdma-core.dev fancy.rdma-core.static ]; } else pkgs.symlinkJoin { name = "sysroot"; postBuild = sysroot-stamp; paths = with pkgs.pkgsHostHost; [ fancy.hwloc.dev fancy.hwloc.static ]; }; clangd-config = pkgs.writeTextFile { name = ".clangd"; text = '' CompileFlags: Add: - "-I${sysroot}/include" - "-Wno-deprecated-declarations" - "-Wno-quoted-include-in-framework-header" ''; executable = false; destination = "/.clangd"; }; crane = import sources.crane { inherit pkgs; }; craneLib = crane.craneLib.overrideToolchain pkgs.rust-toolchain; devroot = pkgs.symlinkJoin { name = "dataplane-dev-shell"; paths = [ clangd-config ] # pkgsBuildBuild (not pkgsBuildHost): dev-shell tools run on, and target, # the build host. pkgsBuildHost is "runs on build, targets host", which # under a cross pkgs (e.g. libc=musl, platform=bluefield3) installs only # target-prefixed binaries (e.g. x86_64-unknown-linux-musl-pkg-config) -- # cargo build scripts that invoke `pkg-config`/`clang` unprefixed then fail # to find them in PATH. ++ (with pkgs.pkgsBuildBuild.llvmPackages'; [ bintools clang libclang.lib lld ]) ++ (with pkgs.pkgsBuildBuild; [ actionlint bash cargo-bolero cargo-deny cargo-depgraph cargo-edit cargo-expand cargo-llvm-cov cargo-mutants cargo-nextest cargo-show-asm commitlint-rs deno direnv duvet gateway-crd gettext iai-callgrind-runner jq just kopium llvmPackages'.clang # you need the host compiler in order to link proc macros llvmPackages'.llvm # needed for coverage markdownlint-cli2 nixfmt npins opengrep openssl oras pinact pkg-config python3Packages.pyflakes qemu-user rust-toolchain shellcheck skopeo static-web-server valgrind wasmtime wget yq zizmor ]); }; # The bootable kernel image filename as linux-fancy emits it. # x86_64 produces a `bzImage`; aarch64 produces a raw `Image`. Both are # installed into the manifest as `vmlinuz`, so nothing downstream has to # care which one this build produced. kernel-image-name = if platform'.arch == "aarch64" then "Image" else "bzImage"; # Guest architecture as spelled in the kernel manifest. Must match # `n_vm::Arch::manifest_name`. kernel-manifest-arch = if platform'.arch == "aarch64" then "aarch64" else "x86_64"; # Directory holding the kernel built from our own config fragments. # # "union" because it carries the union of what the whole test suite needs # -- today from the hand-maintained fragment list in # nix/overlays/dataplane-dev.nix, later checked against the requirements # the tests declare. Named separately from the profiles because several # profiles share one kernel: the artifacts are installed once and # referenced by each. union-kernel-dir = "union"; # Directory for the deliberately-modular kernel (see modular.config). modular-kernel-dir = "modular"; # Directory for the pinned Flatcar release -- the kernel we actually ship # on, and the reason the modular path exists at all. flatcar-kernel-dir = "flatcar"; # Directory for the pinned Ubuntu kernel. A second distro, to test that # the modular path generalises rather than fitting Flatcar in particular. ubuntu-kernel-dir = "ubuntu"; # Every guest kernel that gets installed, keyed by its artifact directory. # # Keyed by *kernel* rather than by profile because several profiles can # share one: the image is installed once and referenced by each. `boot` # is a property of the kernel, not of the profile -- it follows from # whether that kernel can reach its own root. guest-kernels = { ${union-kernel-dir} = { image = "${pkgs.linux-fancy}/${kernel-image-name}"; configfile = pkgs.linux-fancy.configfile; boot = "direct"; initramfs = null; modules = null; modDirVersion = null; }; ${modular-kernel-dir} = { image = "${pkgs.linux-fancy-modular}/${kernel-image-name}"; configfile = pkgs.linux-fancy-modular.configfile; boot = "initramfs"; initramfs = initramfs-modular; modules = pkgs.linux-fancy-modular.modules; inherit (pkgs.linux-fancy-modular) modDirVersion; }; } # Flatcar is pinned to an `amd64-usr` release, so the profile simply does # not exist for another guest architecture. Omitted rather than pointed # at the x86_64 artifacts: the manifest's `arch` is derived from the build # platform, so including it would claim an aarch64 kernel and hand over an # x86_64 one -- a mismatch `check_arch` cannot catch because the manifest # would be lying rather than disagreeing. # # Flatcar does publish arm64 releases; wiring one up is a matter of a # second pin, not new mechanism. // lib.optionalAttrs (kernel-manifest-arch == "x86_64") { ${flatcar-kernel-dir} = { inherit (flatcar-kernel-adapted) image configfile modules modDirVersion ; boot = "initramfs"; initramfs = initramfs-flatcar; }; # Ubuntu publishes arm64 kernels too, so unlike Flatcar this is not # inherently x86_64-only; it is pinned to an `amd64` .deb today and # gated for the same reason -- claiming an aarch64 kernel while handing # over an x86_64 one is a lie `check_arch` cannot catch. Lifting it is # a second set of pins, not new mechanism, but the arm64 `vmlinuz` is a # compressed `Image` and wants checking against QEMU's `-kernel` first. ${ubuntu-kernel-dir} = { inherit (ubuntu-kernel-adapted) image configfile modules modDirVersion ; boot = "initramfs"; initramfs = initramfs-ubuntu; }; }; # The environments a test can run in. # # A profile is a (kernel, hypervisor) pair. Today they differ only in # hypervisor, which is exactly the axis `n-vm/tests/integration.rs` # currently sweeps *by hand* -- `test_which_runs_in_vm_with_iommu` and # `..._with_qemu_iommu` are the same test written twice. Making it a # profile is what lets those collapse. # # Profile names are Rust identifiers because each becomes a module name in # the generated test tree (`some_test::qemu`), so nextest can filter on # one environment. kernel-profiles = { cloud_hypervisor = { hypervisor = "cloud_hypervisor"; kernel-dir = union-kernel-dir; }; qemu = { hypervisor = "qemu"; kernel-dir = union-kernel-dir; }; # The modular kernel, whose virtiofs is a module, so it can only be # reached through an initramfs. QEMU because it is the backend whose # initrd handling we exercise first; a cloud-hypervisor variant is a # one-line addition once that is proven. modular = { hypervisor = "qemu"; kernel-dir = modular-kernel-dir; }; } // lib.optionalAttrs (kernel-manifest-arch == "x86_64") { # The kernel the dataplane actually ships on. The whole point: our own # kernel is built from a config we chose, so it cannot tell us whether # the code works on the one we deploy. x86_64 only -- see # `guest-kernels`. flatcar = { hypervisor = "qemu"; kernel-dir = flatcar-kernel-dir; }; # A distro kernel that is *not* the one we ship on, which is the point: # Flatcar passing tells us the code works where we deploy, but only a # second distro can tell us whether the harness itself generalises. ubuntu = { hypervisor = "qemu"; kernel-dir = ubuntu-kernel-dir; }; }; # The profile used when a test does not name one. default-kernel-profile = "cloud_hypervisor"; # nix's declaration of which guest kernels exist, read by the container # tier (see `n_vm::kernel_manifest`). # # This is the seam that keeps `cargo` from ever having to invoke `nix`: # the artifacts are materialized first (`just setup-roots`), and the tests # only read them. Paths are container-absolute because the container tier # is what consumes them -- every first-level `testroot` entry is # bind-mounted at the container root, so `kernels/` lands at `/kernels`. kernel-manifest = builtins.toJSON { default = default-kernel-profile; profiles = lib.mapAttrs ( _name: profile: let k = guest-kernels.${profile.kernel-dir}; dir = "/kernels/${profile.kernel-dir}"; in { arch = kernel-manifest-arch; inherit (profile) hypervisor; # `boot` follows from the kernel, not from the profile: it is # "direct" when the kernel can mount its own root, and "initramfs" # when the transport that reaches the root is itself a module. inherit (k) boot; kernel = "${dir}/vmlinuz"; config = "${dir}/config"; } // lib.optionalAttrs (k.initramfs != null) { initramfs = "${dir}/initramfs"; } // lib.optionalAttrs (k.modules != null) { modules = "${dir}/modules/${k.modDirVersion}"; } ) kernel-profiles; }; # Minimal derivation containing the bootable kernel image and the # manifest describing it. # # The full linux-fancy output includes modules, headers, etc. that are # not needed inside the test container -- we extract just the bootable # image so that symlinkJoin produces the `kernels/` tree in testroot # without pulling in the rest of the kernel tree. # # IMPORTANT: this is `pkgs.linux-fancy` (the *host*-platform kernel), not # `pkgs.pkgsBuildHost.linux-fancy` (the *build*-platform kernel). The # guest kernel must match the guest (= test binary) architecture. For a # native build the two package sets coincide, so this is a no-op for # x86_64; for a cross build it selects the aarch64 kernel. # The `config` is the kernel's own resolved `.config`, recorded so that a # test's declared kernel requirements can be checked against what this # kernel actually provides -- before booting, with a message naming the # missing symbol rather than a mysterious runtime failure. # # `linux-fancy.configfile` is the merged, dependency-resolved output of # nix/pkgs/linux/merge-config.nix, which is what `linuxManualConfig` built # from. A *foreign* kernel has no such derivation and its config is # recovered from the image with `extract-ikconfig` instead; both land here # under the same name, so nothing downstream has to care which it was. kernel-image = pkgs.runCommand "kernel-image" { } ( '' mkdir -p $out cp ${pkgs.writeText "n-vm-manifest.json" kernel-manifest} \ $out/n-vm-manifest.json '' + lib.concatStrings ( lib.mapAttrsToList (dir: k: '' mkdir -p $out/kernels/${dir} cp ${k.image} $out/kernels/${dir}/vmlinuz cp ${k.configfile} $out/kernels/${dir}/config ${lib.optionalString (k.initramfs != null) '' cp ${k.initramfs}/initramfs $out/kernels/${dir}/initramfs ''} ${lib.optionalString (k.modules != null) '' mkdir -p $out/kernels/${dir}/modules cp -r ${k.modules}/lib/modules/${k.modDirVersion} \ $out/kernels/${dir}/modules/${k.modDirVersion} chmod -R u+w $out/kernels/${dir}/modules ''} '') guest-kernels ) ); # Builds the initramfs for a kernel whose boot-critical drivers are # modules. # # Only needed when the root filesystem transport is `=m`: mounting the # workspace needs virtiofs, virtiofs is a module, and the module tree # lives on the workspace. The initramfs is the only channel that escapes # that, because the kernel unpacks it itself, from memory, before any # driver loads. # # The dependency closure and load order are resolved *here*, by the real # `modprobe` against the real module tree, rather than in the guest. We # know the answer at build time, so the pre-init should not be # rediscovering it at boot: it reads an ordered list and calls # `finit_module` down it. No `modules.dep` parsing, no dependency # resolution, no uevent handling in the VM. # # `boot-modules` are the modules needed to reach the root. Feature # modules a test asks for are loaded later by `n-it`, from the mounted # tree, and do not belong here. mk-initramfs = { kernel, pre-init, boot-modules ? [ "virtiofs" ], }: pkgs.runCommand "n-vm-initramfs" { nativeBuildInputs = with pkgs.pkgsBuildHost; [ cpio kmod xz zstd ]; } '' root=$(mktemp -d) mkdir -p "$root/modules" "$root/newroot" # Ask modprobe for the closure, in load order. `--show-depends` # prints one `insmod ` line per module, dependencies first. for m in ${pkgs.lib.escapeShellArgs boot-modules}; do modprobe --dirname ${kernel.modules} \ --set-version ${kernel.modDirVersion} \ --show-depends "$m" \ || { echo "no such module in the tree: $m" >&2; exit 1; } done | awk '$1 == "insmod" { print $2 }' > "$NIX_BUILD_TOP/ordered" # Dedupe while preserving order: a shared dependency (fuse, here) # appears once per dependent, and loading it twice is an error. : > "$root/modules.load" declare -A seen while read -r ko; do [ -n "$ko" ] || continue base=$(basename "$ko") # Decompress on the way in. A distro tree ships `.ko.xz`, and # `finit_module` cannot read that unless the kernel was built # with CONFIG_MODULE_DECOMPRESS -- which is not something we can # rely on for someone else's kernel. Doing it here means the # pre-init never needs a decompressor. case "$base" in *.ko.xz) base=''${base%.xz}; xz -dc "$ko" > "$root/modules/$base" ;; *.ko.zst) base=''${base%.zst}; zstd -dc "$ko" > "$root/modules/$base" ;; *.ko) cp "$ko" "$root/modules/$base" ;; *) echo "unrecognised module file: $ko" >&2; exit 1 ;; esac [ -n "''${seen[$base]:-}" ] && continue seen[$base]=1 echo "/modules/$base" >> "$root/modules.load" done < "$NIX_BUILD_TOP/ordered" cp ${pre-init} "$root/init" chmod +x "$root/init" mkdir -p $out # The kernel sniffs the initramfs format from its magic bytes, so # the filename carries no information and the compressor is chosen # from what *this* kernel can actually decompress. Read from the # config at build time rather than at eval time, which keeps this # free of import-from-derivation. # # Not assumable: our kernel has CONFIG_RD_GZIP=n and # CONFIG_RD_ZSTD=y, while Flatcar ships a `.cpio.gz`. Guessing # would produce a kernel panic with no useful message. cpio_out=$out/initramfs # `$NIX_BUILD_TOP`, not `/tmp`. A fixed path under `/tmp` is private # to the build only while the sandbox is on; without one -- an # unprivileged container, where nix cannot build the sandbox and # `sandbox-fallback` quietly drops it -- every build shares the host's # `/tmp`, and the three kernels' initramfs derivations build in # parallel. Two `cpio` runs then write one file while a third `zstd` # reads it, which surfaces as `zstd: error 11 : Src size is incorrect` # from whichever one lost. `$root` was always safe: `mktemp -d` # answers under `TMPDIR`, which nix does set per build. cpio=$NIX_BUILD_TOP/initramfs.cpio # Three things otherwise vary between two builds of the same inputs, # and `cpio` records all three, so the compressed image differs every # time and `nix build --rebuild` calls the derivation non-reproducible: # # - mtime. `cp` and the decompressors above stamp the current time. # - uid/gid. A multi-user nix build runs as whichever `nixbld` user # was free, so the archive named e.g. `nixbld1`. Root is also the # right answer on its own merits: this is a root filesystem. # - entry order. `find` walks in readdir order, which is a property # of the filesystem rather than of the tree being packed. # # `--reproducible` covers the device and inode numbers as well. find "$root" -exec touch -h -d @1 {} + (cd "$root" && find . -print0 | sort -z \ | cpio --null -o -H newc --quiet --reproducible --owner 0:0) > "$cpio" if grep -q '^CONFIG_RD_ZSTD=y' ${kernel.configfile}; then zstd -19 -T0 -q -o "$cpio_out" "$cpio" elif grep -q '^CONFIG_RD_GZIP=y' ${kernel.configfile}; then gzip -9 -c "$cpio" > "$cpio_out" elif grep -q '^CONFIG_RD_XZ=y' ${kernel.configfile}; then xz -9 -c --check=crc32 "$cpio" > "$cpio_out" else # Always supported, and a few hundred KB is not worth a panic. cp "$cpio" "$cpio_out" fi cp "$root/modules.load" $out/modules.load ''; # The pre-init, linked statically. # # It runs as PID 1 before /nix/store is mounted, so it cannot be # dynamically linked: its ELF interpreter would name a path that does not # exist yet, and the kernel would fail to exec it. # # Two things are needed and neither is the default. `+crt-static` asks # for a static link; glibc's static archives then have to be found, and # they live in a *separate output* that is not part of the sysroot -- so # without the library path the link fails on `-lc` and friends. # # `overrideAttrs` rather than an argument to `workspace-builder` because # `args` is merged with `//`, which would replace the whole `env` attrset # rather than adding to it, discarding the sysroot and toolchain settings # every other crate depends on. n-preinit-static = workspace."n-preinit".overrideAttrs (orig: { env = orig.env // { # The search path goes through `-Clink-arg=-L`, not `LIBRARY_PATH`. # # `LIBRARY_PATH` is honoured by the *native* cc-wrapper, so it worked # for an x86_64 build and silently did nothing for a cross one: the # target-prefixed wrapper ignores it, and the link failed on `-lc` # with the archives sitting right there in the store. `-Clink-arg` # reaches the linker either way, which is why the sysroot is already # passed that way in nix/profiles.nix. # # The sanitizer comes back out. rustc refuses the combination outright # -- "sanitizer is incompatible with statically linked libc" -- so a # sanitized build otherwise cannot get as far as producing a guest # image, and `just sanitize=address test` dies in `setup-roots` rather # than in anything it was trying to measure. Dropping it here rather # than dropping in-VM testing from sanitized builds is the right trade: # the pre-init is the scaffolding that execs the code under test, not # the code under test. It costs a rebuild of two small crates, its # only dependencies being `n-vm-protocol` and a feature-trimmed `nix`. RUSTFLAGS = lib.concatStringsSep " " ( (lib.subtractLists sanitizer-rustflags ( lib.filter (flag: flag != "") (lib.splitString " " orig.env.RUSTFLAGS) )) ++ [ "-Ctarget-feature=+crt-static" ] # Only glibc keeps its archives in a separate output; musl's are in # `out`, already in the sysroot. Naming `glibc` unconditionally asks # nixpkgs to build glibc with the musl cross compiler, which fails. ++ lib.optional (libc == "gnu") "-Clink-arg=-L${pkgs.pkgsHostHost.glibc.static}/lib" ); }; }); # Flatcar's repackaged kernel, adapted to the shape `mk-initramfs` and the # kernel-image installer expect of a kernel derivation. # # The adapter exists because those two consumers were written against a # nixpkgs kernel: they ask for `.modules`, `.modDirVersion` and # `.configfile`. Flatcar's is not built here, so it has none of them -- # it has a directory layout instead. Mapping it here keeps the consumers # ignorant of which kind of kernel they were handed, which is the point of # the normalized output shape. # Written as a function because there are two of these now: the mapping is # a property of the *layout* the distro packages produce, not of any one # distro, and both `pkgs/flatcar` and `pkgs/ubuntu` deliberately produce # the same one. adapt-distro-kernel = k: { drv = k; configfile = "${k}/config"; # Discovered by the package rather than restated here; a version bump # would otherwise leave the modules under a directory nothing reads. modDirVersion = lib.removeSuffix "\n" (builtins.readFile "${k}/mod-dir-version"); modules = k; image = "${k}/vmlinuz"; }; flatcar-kernel-adapted = adapt-distro-kernel pkgs.flatcar-kernel; ubuntu-kernel-adapted = adapt-distro-kernel pkgs.ubuntu-kernel; # The initramfs for Flatcar's kernel. # # The same derivation as the modular kernel's, given a different kernel -- # which is the point of normalising the shape. Flatcar's modules are # `.ko.xz`, which `mk-initramfs` decompresses on the way in because # Flatcar does not set CONFIG_MODULE_DECOMPRESS. initramfs-flatcar = mk-initramfs { kernel = flatcar-kernel-adapted; pre-init = "${n-preinit-static}/bin/dataplane-n-preinit"; boot-modules = [ "virtiofs" "vmw_vsock_virtio_transport" ]; }; # The initramfs for Ubuntu's kernel. # # Same derivation again, with two differences from Flatcar's that are # handled without new mechanism: the modules are `.ko.zst` rather than # `.ko.xz`, and `fuse` is built in (`CONFIG_FUSE_FS=y`) rather than # modular, so virtiofs pulls in no dependency and the closure is one # module shorter. initramfs-ubuntu = mk-initramfs { kernel = ubuntu-kernel-adapted; pre-init = "${n-preinit-static}/bin/dataplane-n-preinit"; boot-modules = [ "virtiofs" "vmw_vsock_virtio_transport" ]; }; # The initramfs for the modular kernel. initramfs-modular = mk-initramfs { kernel = pkgs.linux-fancy-modular; pre-init = "${n-preinit-static}/bin/dataplane-n-preinit"; # `virtiofs` to reach the root, `vmw_vsock_virtio_transport` because # n-it needs the result channel the moment it starts and cannot load it # itself -- it is the process the channel reports on. modprobe expands # each to its own dependency closure. boot-modules = [ "virtiofs" "vmw_vsock_virtio_transport" ]; }; # The QEMU system emulator for the test VM, always a build-native (host # CI arch) binary that runs in the Docker container. # # The test VMs always run headless (`-nographic`), so QEMU's GUI display # backends (gtk/sdl/vnc/spice/...) are dead weight. Left enabled they # drag gtk4/gtk3/cairo/pango/vte/libepoxy/SDL into every test/dev root. # `nixosTestRunner = true` is nixpkgs' headless "boot a VM" profile: it # disables exactly those backends and its only other effect is a 9p # uid0 patch we never exercise (we mount via vhost-user-fs, not -virtfs). qemu-system = native-pkgs.qemu_test; # Container-tier tools for the scratch-container test infrastructure. # # This derivation provides the binaries needed inside the Docker # container that launches the test VM: the hypervisor(s), virtiofsd, # and a Linux kernel image (bzImage built from config fragments by # the linux-fancy derivation in nix/overlays/dataplane-dev.nix). # # When used with a scratch container, subdirectories of this derivation # (e.g. bin/, lib/) are volume-mounted at their standard container # paths, and top-level files (e.g. bzImage) are bind-mounted at the # container root. The container also mounts /nix/store from the host # so that the symlinks created by symlinkJoin resolve to the actual # binaries and their transitive library dependencies. # # See development/ideam.md for the design rationale. testroot = pkgs.symlinkJoin { name = "dataplane-test-root"; paths = [ native-pkgs.cloud-hypervisor native-pkgs.virtiofsd qemu-system kernel-image ]; }; devenv = pkgs.mkShell { name = "dataplane-dev-shell"; packages = [ devroot ]; inputsFrom = [ sysroot ]; env = { RUSTC_BOOTSTRAP = "1"; DATAPLANE_SYSROOT = "${sysroot}"; C_INCLUDE_PATH = "${sysroot}/include"; LIBRARY_PATH = "${sysroot}/lib"; PKG_CONFIG_PATH = "${sysroot}/lib/pkgconfig"; LIBCLANG_PATH = "${devroot}/lib"; GW_CRD_PATH = "${pkgs.pkgsBuildHost.gateway-crd}/src/fabric/config/crd/bases"; # Pin native cargo invocations (cargo build/clippy/test --doc) to the # same target the dev sysroot is built for. Without this, cargo defaults # to the build-host triple while LIBRARY_PATH/PKG_CONFIG_PATH point at # cross-target libs, and the link picks up a libc that doesn't match the # rust-std it's compiling against (e.g. glibc rust-std + musl libc = # undefined `open64`/`fstat64`/...). CARGO_BUILD_TARGET = rustc-target; # Rust's pkg-config crate refuses cross-target builds by default; opt in # since our PKG_CONFIG_PATH already points at the matching cross sysroot. PKG_CONFIG_ALLOW_CROSS = "1"; # So that a plain `cargo doc` renders math and diagrams like the docs # build does. RUSTDOCFLAGS = rustdoc-flags; }; }; # Nix escaping made the old regexes match unrelated .sh and .patch files. cHeaderFilter = p: _type: lib.hasSuffix ".h" p; # Prose lives at the repository root, under `development/`, and under # `.github/`; Markdown anywhere else is either compiled into a crate's docs by # `include_str!` or sits beside code that is. Excluding by location rather # than listing the `include_str!` targets keeps a new one from having to be # registered here -- the worst case becomes an unnecessary rebuild instead of # a build that cannot find its README. markdownFilter = rel: _type: lib.hasSuffix ".md" rel && lib.hasInfix "/" rel && !(lib.hasPrefix "development/" rel) && !(lib.hasPrefix ".github/" rel); # `.cargo/config.toml` names this script, so include it deliberately. shellFilter = p: _type: lib.hasSuffix ".sh" p; # `cleanSource` does not read gitignore, so `results` needs excluding by hand # or every developer who has built carries a private `src` hash. outputsFilter = p: _type: (p != "target") && (p != "sysroot") && (p != "devroot") && (p != "results") && (p != ".git"); # `builtins.path` and friends hand the filter an absolute path; the markdown # allowlist is written relative to the repository, so strip the root off. src-root = toString ./.; src = pkgs.lib.cleanSourceWith { filter = full-path: t: let p = baseNameOf full-path; rel = lib.removePrefix (src-root + "/") (toString full-path); in (markdownFilter rel t) || (cHeaderFilter p t) || (shellFilter p t) || ((outputsFilter p t) && (craneLib.filterCargoSources full-path t)); src = lib.cleanSource ./.; name = "source"; }; # Keep dependency fingerprints independent of the source store path. # Consumers resolve this relative prefix from the workspace root. src-prefix = "."; # Registry packages carry a checksum in Cargo.lock, which crane turns into a fixed-output # derivation; git dependencies carry only a commit id, which Nix cannot use as an output # hash. Without one, crane warns and clones the repository during evaluation instead, which # blocks evaluation and caches nothing. # # Each git dependency's hash comes from the npins pin of the same GitHub repository at the # revision Cargo.lock locks (by convention named `crate-` and frozen). npins hashes # GitHub's tarball of that commit, which unpacks to the same tree crane's `fetchgit` checks # out as long as the repository has no submodules and no export-ignore/export-subst # attributes; one that gains either fails loudly with a hash mismatch. A git dependency with # no matching pin fails evaluation here, instead of crane's warning and silent fallback. cargoGitOutputHashes = let lock = builtins.fromTOML (builtins.readFile ./Cargo.lock); pins = (builtins.fromJSON (builtins.readFile ./npins/sources.json)).pins; # Cargo.lock percent-encodes query values (e.g. `/` in branch names), and crane matches # `outputHashes` keys against the decoded source escapes = { "%2F" = "/"; "%2f" = "/"; "%40" = "@"; "%2B" = "+"; "%2b" = "+"; "%25" = "%"; }; decode = builtins.replaceStrings (builtins.attrNames escapes) (builtins.attrValues escapes); gitSources = lib.unique ( map (p: decode p.source) (lib.filter (p: lib.hasPrefix "git+" (p.source or "")) lock.package) ); hashFor = source: let parts = builtins.match "git\\+https://github\\.com/([^/]+)/([^?#]+)(\\?[^#]*)?#([0-9a-f]+)" source; owner = builtins.elemAt parts 0; repo = lib.removeSuffix ".git" (builtins.elemAt parts 1); rev = builtins.elemAt parts 3; matching = lib.filterAttrs ( _: pin: (pin.repository.type or null) == "GitHub" && pin.repository.owner == owner && pin.repository.repo == repo && (pin.revision or null) == rev ) pins; in if parts == null then throw "Cargo.lock git source ${source} is not on GitHub, so npins cannot provide its hash" else if matching == { } then throw '' Cargo.lock locks ${owner}/${repo} at ${rev}, but no npins pin has that revision. If the pin exists and `cargo update` moved the lock to the branch head, move the pin too: npins update --frozen crate-${repo} Otherwise pin the locked revision: npins add github ${owner} ${repo} --branch --at ${rev} --name crate-${repo} --frozen'' else (lib.head (lib.attrValues matching)).hash; in lib.genAttrs gitSources hashFor; cargoVendorDir = craneLib.vendorMultipleCargoDeps { cargoLockList = [ ./Cargo.lock "${pkgs.rust-toolchain.passthru.availableComponents.rust-src}/lib/rustlib/src/rust/library/Cargo.lock" ]; outputHashes = cargoGitOutputHashes; }; # Rename per-revision images so the CI push filter keeps them out of Cachix; # each is built once and shipped through GHCR. Not applied to workspace # builds, which are worth substituting. source-volatile = orig: { name = "dataplane-volatile-${orig.name or "${orig.pname}-${orig.version}"}"; allowSubstitutes = false; }; # For wasm32, pkgs is the host nixpkgs (no pkgsCross), so ctarget resolves to the # host platform (e.g. x86_64-unknown-linux-gnu). That means is-cross-compile is # false for wasm, which is intentional: we don't want native cross-compilation # tooling (strip, objcopy, prefixed clang) — cargo + rustc handle wasm natively. # rustc-target is the actual --target we pass to cargo, which diverges from ctarget # only for wasm. ctarget = pkgs.stdenv'.targetPlatform.rust.rustcTarget; rustc-target = if platform == "wasm32-wasip1" then "wasm32-wasip1" else pkgs.stdenv'.targetPlatform.rust.rustcTarget; is-cross-compile = pkgs.stdenv'.buildPlatform.rust.rustcTarget != ctarget; cc = if is-cross-compile then "${ctarget}-clang" else "clang"; strip = if is-cross-compile then "${ctarget}-strip" else "strip"; objcopy = if is-cross-compile then "${ctarget}-objcopy" else "objcopy"; package-list = builtins.fromJSON ( builtins.readFile ( (pkgs.runCommandLocal "package-list" { TOMLQ = "${pkgs.pkgsBuildHost.yq}/bin/tomlq"; JQ = "${pkgs.pkgsBuildHost.jq}/bin/jq"; } ( if platform == "wasm32-wasip1" then '' $TOMLQ -r '.workspace as $ws | [$ws.members[] | select($ws.metadata.package[.].wasm != false) as $p | { ($p): $ws.dependencies[$p].package }] | add' ${src}/Cargo.toml > $out '' else '' $TOMLQ -r '.workspace.members | sort[]' ${src}/Cargo.toml | while read -r p; do $TOMLQ --arg p "$p" -r '{ ($p): .package.name }' ${src}/$p/Cargo.toml done | $JQ --sort-keys --slurp 'add' > $out '' ) ).overrideAttrs source-volatile ) ); version = (craneLib.crateNameFromCargoToml { inherit src; }).version; # The `loom` and `shuttle` features require `panic = "unwind"` (see # nix/profiles.nix), as do test builds. The sysroot needs the matching # panic runtime and std feature, so we build two cargo command prefixes: # `cargo-cmd-prefix` for production code and `cargo-cmd-prefix-tests` # for the nextest archives. mk-needs-unwind = for-tests: for-tests || builtins.elem "loom" cargo-features || builtins.elem "shuttle" cargo-features; needs-unwind = mk-needs-unwind false; needs-unwind-tests = mk-needs-unwind true; mk-cargo-cmd-prefix = unwind: [ "-Zunstable-options" "-Zbuild-std=std,${if unwind then "panic_unwind" else "panic_abort"}" # note: retention of libunwind on non-glibc is correct in spite of the panic=abort; `backtrace` needs a stack # walker even when panic=abort. In the case of glibc, libgcc_s.so fills that role. You can't escape libgcc_s.so # regardless: it is linked to glibc's libc.so anyway. ( "-Zbuild-std-features=backtrace" + (if unwind then ",panic-unwind" else "") + (if libc != "gnu" then ",system-llvm-libunwind" else "") ) "--target=${rustc-target}" ] ++ (if default-features == "false" then [ "--no-default-features" ] else [ ]) ++ ( if cargo-features != [ ] then [ "--features=${builtins.concatStringsSep "," cargo-features}" ] else [ ] ); cargo-cmd-prefix = mk-cargo-cmd-prefix needs-unwind; cargo-cmd-prefix-tests = mk-cargo-cmd-prefix needs-unwind-tests; invoke = { builder, args ? { pname = null; cargoArtifacts = null; }, # Dependency builds retain reusable Cargo artifacts rather than binaries. for-deps ? false, # Whether the build carries the per-commit `tag` as `VERSION`. Only what ships (the # workspace binaries the images and the validator are made of) needs it. Everything else # gets a constant, so a commit that leaves the Rust sources alone rebuilds none of it, and # the merge queue can reuse what a pull request's run already built for the same tree. stamped ? false, # Skip the binary strip/split step for derivations that produce none. no-bins ? false, profile, cargo-nextest, hwloc, llvmPackages', pkg-config, }: (builder ( { inherit src version cargoVendorDir ; doCheck = false; strictDeps = true; dontStrip = true; doRemapPathPrefix = false; # TODO: this setting may be wrong, test with debugger removeReferencesToRustToolchain = true; removeReferencesToVendorDir = true; nativeBuildInputs = [ (pkgs.pkgsBuildHost.kopium) cargo-nextest llvmPackages'.clang llvmPackages'.lld pkg-config ]; buildInputs = [ hwloc ]; env = { # Dependencies do not read VERSION, so keep their derivation stable, and so do the # builds that do not ship; see `stamped`. "dev" is what the code reports when VERSION # is unset. VERSION = if for-deps then "dependencies" else if stamped then tag else "dev"; CARGO_PROFILE = cargo-profile; DATAPLANE_SYSROOT = "${sysroot}"; LIBCLANG_PATH = "${pkgs.pkgsBuildHost.llvmPackages'.libclang.lib}/lib"; C_INCLUDE_PATH = "${sysroot}/include"; LIBRARY_PATH = "${sysroot}/lib"; PKG_CONFIG_PATH = "${sysroot}/lib/pkgconfig"; GW_CRD_PATH = "${pkgs.pkgsBuildHost.gateway-crd}/src/fabric/config/crd/bases"; RUSTC_BOOTSTRAP = "1"; RUSTFLAGS = if rustc-target != "wasm32-wasip1" then builtins.concatStringsSep " " ( profile.RUSTFLAGS ++ [ "-Clinker=${pkgs.pkgsBuildHost.llvmPackages'.clang}/bin/${cc}" "-Clink-arg=--ld-path=${pkgs.pkgsBuildHost.llvmPackages'.lld}/bin/ld.lld" "-Clink-arg=-L${sysroot}/lib" # Keep debug paths stable across revisions. Source readers # must resolve this relative prefix from the workspace root. "--remap-path-prefix==${src-prefix}" ] ) else ""; }; } // args )).overrideAttrs ( orig: if for-deps || no-bins then { # Only dependency builds should retain crane's target archive. doInstallCargoArtifacts = for-deps; postBuild = (orig.postBuild or "") + '' unset RUSTFLAGS; ''; } else { separateDebugInfo = true; # I'm not 100% sure if I would call it a bug in crane or a bug in cargo, but cross compile is tricky here. # There is no easy way to distinguish RUSTFLAGS intended for the build-time dependencies from the RUSTFLAGS # intended for the runtime dependencies. # One unfortunate consequence of this is that if you set platform specific RUSTFLAGS then the postBuild hook # malfunctions. Fortunately, the "fix" is easy: just unset RUSTFLAGS before the postBuild hook actually runs. # We don't need to set any optimization flags for postBuild tooling anyway. postBuild = (orig.postBuild or "") + '' unset RUSTFLAGS; ''; postInstall = (orig.postInstall or "") + ( if rustc-target != "wasm32-wasip1" then # Native binaries keep their DWARF until fixup: `separateDebugInfo` copies it to # `$debug/lib/debug/.build-id/`, where gdb, perf and llvm-symbolizer look it up # by build ID, and postFixup strips the binary once that copy exists. Stripping # here instead would leave that hook only the stripped binary to work from, and # the build-id file it writes would hold no DWARF at all. '' # Inline-header assertions embed DPDK's dev path through __FILE__. # Erase that store reference from runtime binaries without changing their size. # Keep the debug files and their debuglink CRCs intact. # # Two more ride in with the static libraries, and neither is ever followed: # DPDK's `RTE_EAL_PMD_PATH`, which EAL reads only when it was itself loaded as a # shared object, and libnl's `/etc/libnl`, for a `classid` file nobody ships. # Kept, they would put DPDK's shared build, rdma-core and numactl in the image. for f in $out/bin/*; do ${pkgs.pkgsBuildHost.removeReferencesTo}/bin/remove-references-to \ -t ${pkgs.pkgsHostHost.fancy.dpdk.dev} \ -t ${pkgs.pkgsHostHost.fancy.dpdk.out} \ -t ${pkgs.pkgsHostHost.fancy.libnl.out} \ "$f" done '' else '' mkdir -p $debug/bin for f in $out/bin/*; do mv "$f" "$debug/bin/$(basename "$f")" ${pkgs.pkgsBuildHost.binaryen}/bin/wasm-opt "$debug/bin/$(basename "$f")" --strip-debug -O4 -o "$f" # sadly there is no equivalent of gnu-debuglink in wasm world yet done '' ); # `separateDebugInfo` splits with `$OBJCOPY`, which this derivation's native stdenv # sets to the build machine's objcopy. That one cannot read a cross-compiled binary, # and the hook skips what it cannot read, so point it at the target's objcopy. preFixup = (orig.preFixup or "") + lib.optionalString (rustc-target != "wasm32-wasip1") '' export OBJCOPY=${objcopy} ''; postFixup = (orig.postFixup or "") + '' rm -f $out/target.tar.zst '' # `separateDebugInfo` only copies the debug info out (this derivation sets # `dontStrip`), so strip the binaries now, and point each at its debug file. + lib.optionalString (rustc-target != "wasm32-wasip1") '' for f in $out/bin/*; do ${strip} --strip-debug "$f" ${objcopy} --add-gnu-debuglink="$debug/lib/debug/$(basename "$f")" "$f" done ''; } ); # Share one manifest-based dependency build per flag set across workspace # packages and revisions. Production and tests remain separate because their # unwind and development-dependency fingerprints differ. mk-cargo-artifacts = { for-tests, cmd-prefix, deps-profile, }: pkgs.callPackage invoke { builder = craneLib.buildDepsOnly; profile = deps-profile; for-deps = true; args = { pname = if for-tests then "dataplane-tests" else "dataplane"; cargoArtifacts = null; buildPhaseCargoCommand = builtins.concatStringsSep " " ( # Include dev-dependencies required by nextest archives. ( if for-tests then [ "cargo" "test" "--no-run" "--profile=${cargo-profile}" ] else [ "cargo" "build" "--profile=${cargo-profile}" ] ) # Use the consumer package set so excluded members cannot pull native # dependencies that fail to compile for wasm32-wasip1. ++ (map (pname: "--package=${pname}") (builtins.attrValues package-list)) ++ cmd-prefix ); }; }; cargo-artifacts = mk-cargo-artifacts { for-tests = false; cmd-prefix = cargo-cmd-prefix; deps-profile = profile'; }; cargo-artifacts-tests = mk-cargo-artifacts { for-tests = true; cmd-prefix = cargo-cmd-prefix-tests; deps-profile = profile-tests'; }; workspace-builder = { pname ? null, cargoArtifacts ? cargo-artifacts, }: pkgs.callPackage invoke { builder = craneLib.buildPackage; profile = profile'; stamped = true; args = { inherit pname cargoArtifacts; buildPhaseCargoCommand = builtins.concatStringsSep " " ( [ "cargoBuildLog=$(mktemp cargoBuildLogXXXX.json);" "cargo" "build" "--package=${pname}" "--profile=${cargo-profile}" ] ++ cargo-cmd-prefix ++ [ "--message-format json-render-diagnostics > $cargoBuildLog" ] ); }; }; workspace = builtins.mapAttrs ( dir: pname: workspace-builder { inherit pname; } ) package-list; # VM guest root filesystem for the scratch-container test infrastructure. # # This derivation is shared into the VM via virtiofsd and becomes the # guest's root filesystem (mounted as virtiofs with tag "root"). # # It contains: # - The n-it init system binary (runs as PID 1 in the VM). # - glibc and libgcc shared libraries (so dynamically linked test # binaries can run inside the VM). # # The forwarded-environment directory (test-env, see VM_ENV_DIR) is # pre-created for the same reason: container.rs bind-mounts a host # directory holding the NUL-separated KEY=VALUE file there. # # The test binary directory is bind-mounted by container.rs at # /vm.root/test-bin (see VM_TEST_BIN_DIR in n-vm-protocol), so it # appears at /test-bin in the VM guest. The /test-bin directory is # pre-created here so Docker can create the bind mount without needing # to mkdir on the read-only nix store path. # # See development/ideam.md for the design rationale. vmroot = pkgs.runCommand "dataplane-vm-root" { } '' mkdir -p $out/bin $out/lib $out/test-bin $out/test-env # Essential guest directories. # # The VM root filesystem is mounted read-only via virtiofs, so the # kernel cannot create directories on demand. These empty mount # points must exist so that: # # /dev -- kernel auto-mounts devtmpfs (provides /dev/console, # /dev/null, etc. needed by init and test processes) # /proc -- n-it mounts procfs (needed for /proc/cmdline parsing # and general process introspection) # /sys -- n-it mounts sysfs # /tmp -- n-it mounts tmpfs (writable scratch space) # /run -- n-it mounts tmpfs (runtime state) # /etc -- some libc/nss functions expect this to exist # # Without /dev in particular, the kernel logs # "devtmpfs: error mounting -2" and init may fail with ENOEXEC (-8) # because /dev/console cannot be opened. mkdir -p $out/dev $out/proc $out/sys $out/tmp $out/run $out/etc $out/var # /var/run -> /run symlink. # # Many daemons (including DPDK) default to writing runtime state # under /var/run. On a conventional Linux system /var/run is # either a symlink to /run or a tmpfs in its own right. Since our # root filesystem is read-only via virtiofs, we bake the symlink # into the image so that /var/run/dpdk (and friends) resolve to # the writable /run tmpfs mounted by n-it. # # This mirrors what the dataplane container image already does # (see `gateway-rootfs` below). ln -s /run $out/var/run # n-it init system binary. # The cargo package is "dataplane-n-it" but the VM expects the # binary at /bin/n-it (see INIT_BINARY_PATH in n-vm-protocol). ln -s ${workspace."n-it"}/bin/dataplane-n-it $out/bin/n-it # glibc runtime libraries -- needed by dynamically linked test # binaries running inside the VM. for f in ${pkgs.pkgsHostHost.libc.out}/lib/*.so*; do [ -e "$f" ] || continue ln -s "$f" "$out/lib/$(basename "$f")" done # libgcc runtime libraries (libgcc_s.so, etc.), glibc only: musl # binaries unwind with libunwind, and naming `glibc` on a musl host # platform asks nixpkgs to build glibc with the musl cross compiler. ${lib.optionalString (libc == "gnu") '' for f in ${pkgs.pkgsHostHost.glibc.libgcc}/lib/*.so*; do [ -e "$f" ] || continue ln -s "$f" "$out/lib/$(basename "$f")" done ''} # Create a real /nix/store directory (empty mount point). # # The container tier bind-mounts the host's /nix/store here so that # virtiofsd serves it as a real directory to the VM guest. This # replaces the previous /nix -> /nix absolute symlink, which caused # ELOOP (error -40) inside the guest: the FUSE protocol returns # symlinks to the guest kernel for resolution, and /nix -> /nix is # self-referential from the guest's VFS perspective. # # Nix-built test binaries have rpaths like # /nix/store/{hash}-glibc-X.Y/lib; with /nix/store bind-mounted # through virtiofsd, those paths resolve correctly inside the VM. mkdir -p $out/nix/store # Empty mount point for the host's cargo workspace (see # VM_WORKSPACE_DIR in n-vm-protocol). The container tier bind-mounts # the workspace root here and n-it makes it the test process's working # directory, so that paths captured at compile time relative to the # workspace root -- `file!()`, which bolero records and later # canonicalizes to find its corpus -- resolve inside the guest. # # Pre-created for the same reason as /nix/store above: this derivation # is a read-only nix store path, so Docker cannot create the mount # point itself. # Must match VM_WORKSPACE_DIR in n-vm-protocol. mkdir -p $out/workspace ''; workspace-check = { pname ? null, cargoArtifacts ? cargo-artifacts, }: pkgs.callPackage invoke { builder = craneLib.buildPackage; profile = profile'; args = { inherit pname cargoArtifacts; buildPhaseCargoCommand = builtins.concatStringsSep " " ( [ "cargoBuildLog=$(mktemp cargoBuildLogXXXX.json);" "cargo" "check" "--package=${pname}" "--profile=${cargo-profile}" ] ++ cargo-cmd-prefix ++ [ "--message-format json-render-diagnostics > $cargoBuildLog" ] ); }; }; check = builtins.mapAttrs ( dir: pname: workspace-check { inherit pname; } ) package-list; # Every package checked on its own, all targets, in one derivation. `cargo check --package` # unifies features over the selected package only, so a package (or one of its tests) that # compiles only thanks to features another member turns on fails here, which is what building # each package's tests separately used to catch, at a fraction of the cost: nothing is linked or # run, and the dependency artifacts and target directory are unpacked once rather than once per # package. Not for wasm32, whose members' tests do not build there; `check` covers it. check-each = pkgs.callPackage invoke { builder = craneLib.mkCargoDerivation; profile = profile-tests'; no-bins = true; args = { pname = "check-each"; cargoArtifacts = cargo-artifacts-tests; buildPhaseCargoCommand = builtins.concatStringsSep "\n" ( map ( pname: builtins.concatStringsSep " " ( [ "cargo" "check" "--all-targets" "--package=${pname}" "--profile=${cargo-profile}" ] ++ cargo-cmd-prefix-tests ) ) (builtins.attrValues package-list) ); }; }; test-builder = { package ? null, cargoArtifacts ? cargo-artifacts-tests, }: let pname = if package != null then package else "all"; # init's VM test runs the real image. Its path is compiled into the test, and linked from the # archive's output, since nix cannot read references out of the compressed archive itself. # Not under a model checker's features: the image's own crates do not have them, and only # model-checked tests run then anyway. Not in instrumented builds either, whose image # closure carries the whole toolchain. wants-gateway = cargo-features == [ ] && !(builtins.elem "coverage" instrumentations) && (package == null || package == "dataplane-init"); in pkgs.callPackage invoke { builder = craneLib.mkCargoDerivation; profile = profile-tests'; args = { inherit pname cargoArtifacts; } // pkgs.lib.optionalAttrs wants-gateway { DATAPLANE_TEST_GATEWAY_ROOTFS = "${gateway-rootfs}"; } // { buildPhaseCargoCommand = (builtins.concatStringsSep " " ( [ "mkdir -p $out;" "cargo" "nextest" "archive" "--archive-file" "$out/${pname}.tar.zst" "--cargo-profile=${cargo-profile}" ] ++ (if package != null then [ "--package=${pname}" ] else [ ]) ++ cargo-cmd-prefix-tests )) # Record the remapped source root without changing normal archives. + ( if builtins.elem "coverage" instrumentations then "; echo -n '${src-prefix}' > $out/source-prefix" else "" ) + (if wants-gateway then "; ln -s ${gateway-rootfs} $out/gateway-rootfs" else ""); }; }; tests = { all = test-builder { }; pkg = builtins.mapAttrs ( dir: package: test-builder { inherit package; } ) package-list; }; # Build criterion bench binaries without running them. bench-builder = { package ? null, cargoArtifacts ? cargo-artifacts-tests, }: let pname = if package != null then package else "all"; in pkgs.callPackage invoke { builder = craneLib.mkCargoDerivation; profile = profile-tests'; args = { inherit pname cargoArtifacts; buildPhaseCargoCommand = (builtins.concatStringsSep " " ( [ "mkdir -p $out/bin;" "cargoBenchLog=$(mktemp cargoBenchLogXXXX.json);" "cargo" "bench" "--no-run" "--profile=${cargo-profile}" ] ++ (if package != null then [ "--package=${pname}" ] else [ ]) ++ cargo-cmd-prefix-tests ++ [ "--message-format=json-render-diagnostics > $cargoBenchLog;" ] )) + '' for exe in $(grep -E '"kind":\["bench"\]' "$cargoBenchLog" | grep -oE '"executable":"[^"]+"' | sed -E 's/"executable":"//; s/"$//'); do cp "$exe" "$out/bin/$(basename "$exe" | sed -E 's/-[0-9a-f]{16}$//')" done ''; }; }; benches = bench-builder { }; # Preserve all-target linting, using the test profile and dependency artifacts # required by test and benchmark targets. clippy-builder = { package ? null, }: let pname = if package != null then package else "all"; clippy-cmd = features: builtins.concatStringsSep " " ( [ "cargo" "clippy" "--all-targets" ] ++ features ++ [ "--profile=${cargo-profile}" ] # The platform-aware list excludes members that cannot build for WASI. ++ ( if package != null then [ "--package=${pname}" ] else map (p: "--package=${p}") (builtins.attrValues package-list) ) ++ cargo-cmd-prefix-tests ++ [ "--" "-D warnings" ] ); in pkgs.callPackage invoke { builder = craneLib.mkCargoDerivation; profile = profile-tests'; args = { inherit pname; cargoArtifacts = cargo-artifacts-tests; # The "loom" feature, enabled by --all-features, compiles out most of the dataplane binary, # so we also lint with default features. buildPhaseCargoCommand = '' ${clippy-cmd [ "--all-features" ]} ${clippy-cmd [ ]} ''; }; }; # Workspace source invalidates every package together, so share one artifact # unpack instead of paying the fixed cost per package. clippy = { all = clippy-builder { }; pkg = builtins.mapAttrs (dir: package: clippy-builder { inherit package; }) package-list; }; # Cargo cannot build doctests without running them, so execute them in the # sandbox instead of trying to archive them for the host. doctest-builder = { package ? null, }: let pname = if package != null then package else "all"; in pkgs.callPackage invoke { builder = craneLib.mkCargoDerivation; profile = profile-tests'; no-bins = true; args = { inherit pname; cargoArtifacts = cargo-artifacts-tests; # `cargo test --doc` runs rustdoc, which does not inherit rustc's # registered cfg declarations either. RUSTDOCFLAGS = "-D warnings --check-cfg=cfg(emulated) --check-cfg=cfg(instrumented) --check-cfg=cfg(sanitized)"; # The sandbox cannot resolve the runner's `/usr/bin/env bash` shebang. preBuild = "patchShebangs scripts/test-runner.sh"; buildPhaseCargoCommand = builtins.concatStringsSep " " ( [ "cargo" "test" "--doc" "--profile=${cargo-profile}" ] ++ (if package != null then [ "--package=${pname}" ] else [ ]) ++ cargo-cmd-prefix-tests ); }; }; doctests = { all = doctest-builder { }; pkg = builtins.mapAttrs (dir: package: doctest-builder { inherit package; }) package-list; }; # Flags for rustdoc, shared by the docs build and the dev shell's # RUSTDOCFLAGS. Rustdoc does not inherit rustc's registered cfg # declarations, and `--html-in-header` injects the KaTeX and Mermaid loaders # that render ```math and ```mermaid blocks (scripts/update-doc-headers.sh # regenerates the header when those pins move). The header is a store path, # not a relative one, because rustdoc runs in each crate's own directory: # a relative path breaks `cargo doc` on every dependency. rustdoc-flags = "--check-cfg=cfg(emulated) --check-cfg=cfg(instrumented) --check-cfg=cfg(sanitized) --html-in-header ${./scripts/doc/custom-header.html}"; docs-builder = { package ? null, }: let pname = if package != null then package else "all"; in pkgs.callPackage invoke { builder = craneLib.mkCargoDerivation; profile = profile'; no-bins = true; args = { inherit pname; cargoArtifacts = cargo-artifacts; # `--enable-index-page` writes a top-level index.html listing every crate, so the output has one place to start # browsing from. RUSTDOCFLAGS = "-D warnings -Zunstable-options --enable-index-page ${rustdoc-flags}"; buildPhaseCargoCommand = builtins.concatStringsSep " " ( [ "cargo" "doc" "--profile=${cargo-profile}" "--no-deps" ] ++ (if package != null then [ "--package=${pname}" ] else [ ]) ++ cargo-cmd-prefix ); # The HTML is the whole product; nothing else in the target directory is worth keeping. installPhaseCommand = '' mkdir -p $out cp -r target/${rustc-target}/doc/. $out/ test -f $out/index.html ''; }; }; docs = { all = docs-builder { }; pkg = builtins.mapAttrs ( dir: package: docs-builder { inherit package; } ) package-list; }; # The gateway image's root filesystem: everything but /nix/store, which is only reached through # the symlinks here. Nix reads those as references, so this tree's closure *is* the image's # store, and three things consume it as such: # # - `containers.dataplane` copies it in as the image's top layer and gets the store layers from # its closure; # - init's end-to-end `--supervise-frr` VM test runs it in place, over the host's /nix/store; # - `closure-check.dataplane` reads that closure. gateway-rootfs = (pkgs.stdenv'.mkDerivation { pname = "dataplane-gateway-rootfs"; inherit version; dontUnpack = true; src = null; dontPatchShebangs = true; dontFixup = true; dontPatchElf = true; buildPhase = let # FRR, and the pieces that carry configuration into it, composed once so that the # collisions between busybox's applets, coreutils and FRR's own binaries are # `buildEnv`'s problem rather than this build phase's. # # This has to be laid down as a *tree*, not merely reached through the store, because # FRR is configured with absolute image paths and nothing else will do: # `--bindir=/bin`, `--libdir=/lib`, `--sbindir=/libexec/frr`, `--sysconfdir=/etc`, # `--localstatedir=/run/frr` and `--with-moduledir=/lib/frr/modules` # (nix/pkgs/frr/default.nix). zebra looks for `hh_dplane` at `/lib/frr/modules` and # will not look anywhere else. # # The same set as `containers.frr.dataplane` less `tini`: `dataplane-init` is pid 1 in # this image and reaps its own orphans, which is the whole reason the two images # became one. frr-env = pkgs.buildEnv { name = "dataplane-gateway-frr-env"; # No `/share`: the only things in it are locale, man pages and bash completions, and # FRR's own data lives under its `--prefix`. pathsToLink = [ "/bin" "/etc" "/lib" "/libexec" ]; paths = with pkgs; [ bash coreutils fancy.dplane-plugin fancy.dplane-rpc fancy.frr-agent fancy.frr-config fancy.frr.dataplane findutils gnugrep iproute2 # Runs as a sidecar in the merged pod rather than under the supervisor: it reads # FRR's vty sockets, which are files, so it does not care which network namespace # FRR ends up in -- and it is a metrics endpoint, whose fate should not be the # gateway's. prometheus-frr-exporter python3Minimal ]; }; # mstlink, for the dataplane's physical layer readings on NVIDIA/Mellanox NICs. mstlink = pkgs.pkgsHostHost.fancy.mstlink; in '' mkdir -p "$out/"{bin,lib,var,etc,run/dataplane,run/frr/hh,run/netns,home,tmp} ln -s /run "$out/var/run" for f in "${pkgs.pkgsHostHost.dockerTools.fakeNss}/etc/"* ; do cp --archive "$(readlink -e "$f")" "$out/etc/$(basename "$f")" done ln -s "${workspace.dataplane}/bin/dataplane" "$out/bin/dataplane" ln -s "${workspace.cli}/bin/cli" "$out/bin/cli" ln -s "${workspace.init}/bin/dataplane-init" "$out/bin/dataplane-init" ln -s "${mstlink}/bin/mstlink" "$out/bin/mstlink" for i in "${pkgs.pkgsHostHost.busybox}/bin/"*; do ln -s "${pkgs.pkgsHostHost.busybox}/bin/busybox" "$out/bin/$(basename "$i")" done ln -s "${workspace.dataplane}/bin/dataplane" "$out/dataplane" ln -s "${workspace.init}/bin/dataplane-init" "$out/dataplane-init" ln -s "${workspace.cli}/bin/cli" "$out/dataplane-cli" # FRR, laid over busybox rather than beside it: where both provide a name -- `ip`, most # of coreutils -- the full implementation wins, as it does in the FRR image this # replaces. `frrcommon.sh` and `watchfrr.sh` were written against those, not against # busybox's approximations of them. for i in "${frr-env}/bin/"*; do ln -sf "$i" "$out/bin/$(basename "$i")" done # Real directories holding symlinks, which is `buildEnv`'s own shape and not an # accident of it: FRR creates files beside its configuration and its state, and a # directory that is itself a symlink into the store is one it cannot write into. mkdir -p "$out/libexec" cp --archive "${frr-env}/lib/." "$out/lib/" cp --archive "${frr-env}/libexec/." "$out/libexec/" # `--remove-destination`, because `fakeNss` was copied here first and arrived read-only # from the store. Its `/etc/passwd` has never heard of `frr`, which is the user every # FRR daemon drops to, so this file has to be FRR's rather than merged with it. cp --archive --remove-destination "${frr-env}/etc/." "$out/etc/" ''; }).overrideAttrs source-volatile; containers.dataplane = (pkgs.dockerTools.buildLayeredImage { name = "ghcr.io/githedgehog/dataplane"; inherit tag; # Copied, not `contents`: `contents` is `symlinkJoin`ed, which would make every directory # writable and every symlink here a symlink to a symlink. A copy keeps the tree as it is # in the store -- read-only, its links pointing straight at their targets -- and still # names every target, so the store layers are the tree's closure and nothing more. fakeRootCommands = '' cp --archive --no-preserve=ownership ${gateway-rootfs}/. . ''; config.Entrypoint = [ "/bin/dataplane-init" ]; }).overrideAttrs source-volatile; containers.dataplane-debugger = (pkgs.dockerTools.buildLayeredImage { name = "ghcr.io/githedgehog/dataplane/debugger"; inherit tag; contents = pkgs.buildEnv { name = "dataplane-debugger-env"; pathsToLink = [ "/bin" "/etc" "/var" "/lib" ]; paths = [ pkgs.pkgsBuildHost.gdb pkgs.pkgsBuildHost.rr pkgs.pkgsBuildHost.coreutils pkgs.pkgsBuildHost.bashInteractive pkgs.pkgsBuildHost.iproute2 pkgs.pkgsBuildHost.ethtool pkgs.pkgsHostHost.dockerTools.usrBinEnv pkgs.pkgsHostHost.libc.debug workspace.cli.debug workspace.dataplane.debug workspace.init.debug ]; }; }).overrideAttrs source-volatile; debug-tools = pkgs: [ ## Packages which might be helpful for debugging but aren't enabled by default. ## Uncomment them as needed, but be mindful of container size please. # pkgs.dmidecode # pkgs.emacs # pkgs.gdb # TODO: consider a way to let the user pick gdb' from dev-pkgs (works better in vm) # pkgs.neovim # pkgs.rr # pkgs.valgrind # pkgs.wireshark-cli pkgs.bashInteractive pkgs.coreutils pkgs.curl pkgs.debianutils pkgs.dockerTools.usrBinEnv pkgs.ethtool pkgs.findutils pkgs.gawk pkgs.gnugrep pkgs.gnused pkgs.gnutar pkgs.gzip pkgs.htop pkgs.iproute2 pkgs.iptables pkgs.iputils pkgs.jq pkgs.less pkgs.libc.bin pkgs.libc.out pkgs.man pkgs.nano pkgs.procps pkgs.tcpdump pkgs.util-linux pkgs.vim pkgs.wget pkgs.yq pkgs.zstd ] ++ lib.optionals (libc == "gnu") [ pkgs.pkgsHostHost.glibc.libgcc ]; containers.debug-tools = (pkgs.dockerTools.buildLayeredImage { name = "debug-tools"; tag = "dev"; # don't push or tag this with anything that might end up in the production repo contents = pkgs.buildEnv { name = "debug-tools-env"; pathsToLink = [ "/bin" "/etc" "/lib" "/libexec" "/share" "/tmp" "/usr" "/var" ]; paths = debug-tools pkgs; }; fakeRootCommands = '' #!${pkgs.bash}/bin/bash set -euo pipefail mkdir -p /{bin,lib,var,etc,run/dataplane,run/frr/hh,run/netns,home,tmp} ln -s /run /var/run # symlinks to help imitate the real image ln -s /bin/dataplane /dataplane ln -s /bin/cli /dataplane-cli ln -s /bin/dataplane-init /dataplane-init ''; enableFakechroot = true; }).overrideAttrs source-volatile; # The FRR image's contents, which closure-check reads: the image itself is # compressed, so nix finds no store references in it. frr-dataplane-contents = pkgs.buildEnv { name = "dataplane-frr-env"; pathsToLink = [ "/bin" "/etc" "/lib" "/libexec" "/share" "/usr" "/var" ]; paths = with pkgs; [ bash coreutils dockerTools.usrBinEnv fancy.dplane-plugin fancy.dplane-rpc fancy.frr-agent fancy.frr-config fancy.frr.dataplane findutils gnugrep iproute2 jq prometheus-frr-exporter python3Minimal tini ]; }; containers.frr.dataplane = (pkgs.dockerTools.buildLayeredImage { name = "ghcr.io/githedgehog/dataplane/frr"; inherit tag; contents = frr-dataplane-contents; fakeRootCommands = '' #!${pkgs.bash}/bin/bash set -euxo pipefail mkdir /tmp mkdir -p /run/frr/hh chown -R frr:frr /run/frr mkdir -p /var ln -s /run /var/run chown -R frr:frr /var/run/frr rm /etc/passwd /etc/group cp ${pkgs.fancy.frr-config}/etc/passwd /etc/passwd cp ${pkgs.fancy.frr-config}/etc/group /etc/group ''; enableFakechroot = true; config.Entrypoint = [ "/bin/tini" "--" ]; config.Cmd = [ "/libexec/frr/docker-start" ]; }).overrideAttrs ( orig: source-volatile orig // { passthru = (orig.passthru or { }) // { contents = frr-dataplane-contents; }; } ); containers.frr.host = (pkgs.dockerTools.buildLayeredImage { name = "ghcr.io/githedgehog/dataplane/frr-host"; inherit tag; contents = pkgs.buildEnv { name = "dataplane-frr-host-env"; pathsToLink = [ "/bin" "/etc" "/lib" "/libexec" "/share" "/usr" "/var" ]; paths = with pkgs; [ bash coreutils dockerTools.usrBinEnv # TODO: frr-config's docker-start launches /bin/frr-agent which is not # present in the host container. A host-specific entrypoint script may # be needed once this container is actively deployed. fancy.frr-config fancy.frr.host findutils gnugrep iproute2 jq prometheus-frr-exporter python3Minimal tini ]; }; fakeRootCommands = '' #!${pkgs.bash}/bin/bash set -euxo pipefail mkdir /tmp mkdir -p /run/frr/hh chown -R frr:frr /run/frr mkdir -p /var ln -s /run /var/run chown -R frr:frr /var/run/frr rm /etc/passwd /etc/group cp ${pkgs.fancy.frr-config}/etc/passwd /etc/passwd cp ${pkgs.fancy.frr-config}/etc/group /etc/group ''; enableFakechroot = true; config.Entrypoint = [ "/bin/tini" "--" ]; config.Cmd = [ "/libexec/frr/docker-start" ]; }).overrideAttrs source-volatile; # What ships, checked against what has no business shipping. # # A container image's contents are decided by nix's reference scanner, which # reads store hashes out of file *contents* -- so a single path in a debug # string, a libtool archive or a `__FILE__` quietly adds that path's whole # closure. Nothing warns, and the damage does not show up in review: it shows # up months later as an image that is inexplicably a few hundred megabytes # larger than the binaries in it. Two measured instances, both of them one # file reaching for one small thing: # # * FRR's `-latomic` resolved through `libgccjit`, whose other 137 MB came # along for a 20 KB library (nix/overlays/frr.nix, `libatomic`). # * `rte_pause.h` named in an assertion string dragged in DPDK's `dev` # output -- headers, at runtime (see the `remove-references-to` note in # the crane `postInstall` above). # # So this is a gate rather than a report. Once a build tool is out of the # image, the way to keep it out is to make its return a build failure at the # commit that causes it, not a size graph somebody notices later. # # Deliberately matched on path *names* rather than on a pinned set of # derivations: the point is to catch a toolchain arriving by a route nobody # anticipated, and naming the routes we already know about would only catch # the ones we have already fixed. # # One check per published image, named like the image's `just build-container` # target: `closure-check.dataplane` and `closure-check.frr.dataplane`. closure-check = let # Extended regexes, matched against the name half of each store path. forbidden = [ { pattern = "(^|-)(clang|llvm|libclang)"; why = "a compiler; reaches an image through debug info or an assertion string"; } { pattern = "(^|-)(rustc|rust-minimal|rust-toolchain)"; why = "the Rust toolchain; `removeReferencesToRustToolchain` should have blanked this"; } { pattern = "(^|-)(vendor-cargo-deps|cargo-)"; why = "vendored crate sources; `removeReferencesToVendorDir` should have blanked this"; } { pattern = "(^|-)python3"; why = "an interpreter: one more thing in the image able to run code it was not built with. FRR needs one and is exempted below; nothing else gets to arrive quietly beside it"; } { pattern = "(^|-)(binutils|gcc-wrapper|cmake|meson|ninja|pkg-config|autoconf|automake)"; why = "a build tool"; } { pattern = "-dev$"; why = "a `dev` output: headers, pkg-config files and static archives, none of which a running binary reads"; } { pattern = "^source$|-source$"; why = "a source tree, usually via `-ffile-prefix-map` landing in a runtime string"; } ]; # Names that every one of these rules must agree are bad. # # A matcher that matches nothing passes this check silently, and a # gate that cannot fail is worse than no gate: it reports a clean # image forever. This ran vacuously once already -- the patterns were # escaped for a shell they were never passed to, so every comparison # was against a quoted string that no name could equal -- and the # build said "clean" over a closure with two source trees in it. So # the check now proves it can fail before it is allowed to pass. canaries = [ "clang-19.1.7" "rust-minimal-1.98.0" "vendor-cargo-deps" "python3-minimal-3.14.7" "cmake-3.31.7" "dpdk-v26.07-hh-dev" "source" ]; # Names that trip a rule and ship anyway, each with the reason it is not # the thing that rule is looking for. # # An exemption is deliberately not a rule change. The pattern still # matches, the canary that proves the pattern works still fires, and # anything else matching it still fails the build -- what changes is that # this one name is permitted in the closure. Widening the pattern instead # would have retired the rule quietly. # # Every exemption has to be *used*, or the build fails. An exemption # nobody needs is a hole nobody is watching: if FRR ever stops reaching # for an interpreter, this should be deleted at that commit rather than # left behind to cover the next thing that reaches for one. exempt = [ { pattern = "^python3-minimal-"; why = "FRR's `frr-reload.py` is how any configuration reaches FRR at all, and its `#!` line names this interpreter; both images ship FRR"; } ]; # Names that must *not* trip any rule, so a pattern cannot be widened # into one that flags the whole image. allowed = [ "glibc-2.42-84" "gcc-15.3.0-lib" "gcc-15.3.0-libgcc" "dpdk-v26.07-hh" "dataplane-0.27.0" "busybox-1.37.0" "rdma-core-fix-lto-64.0" "libnl-3.12.0" ]; # Tab-separated, and *not* shell-escaped: this is read by `read`, which # would take the quotes as part of the value. # # Every line is newline-*terminated*, not newline-separated. `while read` # returns non-zero at EOF, so a final line without a newline is read into # the variable and then dropped by the loop condition. `concatStringsSep` # produces exactly that shape, and it cost a silent false pass here: the # last rule and the last canary were both discarded, so the check proved # itself working and then failed to look for a source tree. lines = f: xs: pkgs.lib.concatMapStrings (x: "${f x}\n") xs; rules = lines (r: "${r.pattern}\t${r.why}") forbidden; # `attr` is the image's attribute path in default.nix, for the error message. check = name: attr: image: pkgs.runCommandLocal "${name}-closure-check" { closure = pkgs.closureInfo { rootPaths = [ image ]; }; inherit rules; canaries = lines (c: c) canaries; allowed = lines (a: a) allowed; exemptions = lines (e: "${e.pattern}\t${e.why}") exempt; passAsFile = [ "rules" "canaries" "allowed" "exemptions" ]; } '' set -euo pipefail # Prints every rule the given name trips, one "patternwhy" per line. matches() { local name="$1" while IFS=$'\t' read -r pattern why; do [ -n "$pattern" ] || continue # `-e`, because a pattern may legitimately begin with `-` # (`-dev$`), which grep would otherwise read as an option. if printf '%s' "$name" | grep -Eq -e "$pattern"; then printf '%s\t%s\n' "$pattern" "$why" fi done < "$rulesPath" } # Prove the matcher works before trusting it to say "clean". while read -r canary; do [ -n "$canary" ] || continue if [ -z "$(matches "$canary")" ]; then echo "closure-check is broken: '$canary' should have been flagged and was not." >&2 exit 1 fi done < "$canariesPath" while read -r ok; do [ -n "$ok" ] || continue hit="$(matches "$ok" || true)" if [ -n "$hit" ]; then echo "closure-check is too greedy: '$ok' is legitimate but matched:" >&2 printf '%s\n' "$hit" >&2 exit 1 fi done < "$allowedPath" # The exemption, if any, that permits this name. Prints "patternwhy". exemption() { local name="$1" while IFS=$'\t' read -r pattern why; do [ -n "$pattern" ] || continue if printf '%s' "$name" | grep -Eq -e "$pattern"; then printf '%s\t%s\n' "$pattern" "$why" return fi done < "$exemptionsPath" } status=0 count=0 used="" while read -r path; do count=$((count + 1)) rest="''${path#/nix/store/}" name="''${rest#*-}" hit="$(matches "$name" || true)" [ -n "$hit" ] || continue pass="$(exemption "$name" || true)" if [ -n "$pass" ]; then used="$used''${pass%%$'\t'*}"$'\n' echo "permitted in the runtime closure: $name" >&2 echo " ''${pass#*$'\t'}" >&2 continue fi status=1 echo "forbidden in the runtime closure: $name" >&2 printf '%s\n' "$hit" | while IFS=$'\t' read -r _ why; do echo " $why" >&2 done echo " $path" >&2 echo >&2 done < "$closure/store-paths" # An exemption nobody needed is one nobody is watching. See the comment # on `exempt` in default.nix. while IFS=$'\t' read -r pattern _; do [ -n "$pattern" ] || continue if ! printf '%s' "$used" | grep -Fqx -- "$pattern"; then echo "closure-check has a stale exemption: nothing in the closure matches '$pattern'." >&2 echo "Delete it from \`exempt\` in default.nix; the rule it covers is doing its job again." >&2 exit 1 fi done < "$exemptionsPath" if [ "$status" -ne 0 ]; then echo "${attr}'s runtime closure contains build-only paths (listed above)." >&2 echo "To find who pulled one in:" >&2 echo " nix why-depends --all \$(nix build -f default.nix ${attr} --print-out-paths --no-link) " >&2 echo "Then either stop creating the reference, or blank it with remove-references-to." >&2 exit 1 fi echo "runtime closure clean: $count paths, none build-only" touch "$out" ''; in { dataplane = check "dataplane" "gateway-rootfs" gateway-rootfs; frr.dataplane = check "frr-dataplane" "containers.frr.dataplane.contents" containers.frr.dataplane.contents; }; in { inherit benches check check-each closure-check clippy containers devenv devroot gateway-rootfs doctests docs mk-initramfs package-list pkgs sources src sysroot testroot tests vmroot workspace ; profile = profile'; platform = platform'; }