24 sep 2025
this post explores integrating nix directly into cargo as a continuation of the earlier rust abstractions post.
unlike previous attempts like cargo2nix
,
crate2nix
, crane
, and naersk
to
adapt cargo builds to nix tooling, this libnix initiative seeks to
enhance cargo
with an inherent nix backend.
so, what is cargo
exactly?
cargo encompasses the following features:
cargo
includes
rustc
and may need external tools like
ld
/lld
for linking or compilers such as
gcc
for -sys crates.this prototype can successfully compile these crates:
screencast of
cargo build
with libnix backend
unfortunately, i lack the financial resources needed to pursue further development.
curious about the source code? explore the README.md for a starting point.
this map is the result of weeks of reading the cargo source code and figuring out where to place the nix build backend.
crate
compilation gets more fine grained, so CI can also
serve intermediate artifacts (new)nix
, i.e. only the crates with -sys need
to have custom libraries and compilers exposed to them (new)rm ./target/
as it is moved into /nix/store
and nix-collect-garbage
is used for cleanups (new)Cargo.toml
/Cargo.lock
in a reproducible way
but that would require a nix implementationcargo build
with the nix backend is not
reproducibleboth imags below illustrate the new locations for files / artifacts used to build the cargo project using libnix.
in cargo with libnix this folder is still in use. conceptionally this should also move into the /nix/store and then symlinked:
~/.cargo/registry/cache/index.crates.io-6f17d22bba15001f/pkcs8-0.10.2.crate
gzip compressed source code of pkcs8 dependency
~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/pkcs8-0.10.2/
uncompressed source code so cargo can compile it
~/.cargo/registry/index/index.crates.io-6f17d22bba15001f/.cache/
a copy of crates.io index
so crate sources currently are downloaded twice.
getting the build.rs concept working in nix took a huge amount of work!
these things were hard to overcome:
passthru
+ referencing files from the previous
stage ${crate}/nix/rustc-arguments
/nix/rustc-arguments
concept.allCollectedInputs = a: lib.unique (
builtins.foldl' (acc: el: acc ++ [el] ++ (allCollectedInputs el.rust_crate_libraries)) [] a
);
rustc_linker_arguments = rust_crate_libraries: builtins.concatStringsSep " " (map (lib: "-L ${lib}") (allCollectedInputs rust_crate_libraries));
rustc_arguments = rust_crate_parent: assert lib.assertMsg (builtins.length rust_crate_parent <= 1) "passthru.rust_crate_parent must have 0 or 1 element at maxium" ;
lib.replaceStrings ["\n"] [""] (builtins.concatStringsSep " " (map (crate:
if builtins.pathExists "${crate}/nix/rustc-arguments" then
builtins.readFile "${crate}/nix/rustc-arguments"
else
""
) rust_crate_parent));
rustc_propagated_arguments = rust_crate_libraries: lib.replaceStrings ["\n"] [""] (builtins.concatStringsSep " " (map (crate:
if builtins.pathExists "${crate}/nix/rustc-propagated-arguments" then
builtins.readFile "${crate}/nix/rustc-propagated-arguments"
else
""
) (allCollectedInputs rust_crate_libraries)));
environment_variables = rust_script_build_run: builtins.concatStringsSep " " (map (d: "${d}/nix/environment-variables") rust_script_build_run);
get_rust_crate_parent = parent_list:
assert lib.assertMsg (builtins.length parent_list == 1) "item is supposed to have exactly one parent, while it has 0 or more than 1";
builtins.head parent_list;
logone is a custom
logger crate library and standalone program written in rust, inspired by
https://github.com/maralorn/nix-output-monitor at the
high cost of reverse-engineering of the internal-json
log
format in nix.
logone became the successor of a proposed concept from 2014 it seems!
this is how logone displays the @nix / @cargo output to the user:
it converts this verbose nix trash outputs:
nix build ... -v
(i like this, except i never get
warnings)nix build ... -L
(way too verbose)into something one actualy can consume as humans (it
is very similar to cargo
’s output):
cargo build
with libnix backendcargo build
with libnix backend (showing an error)in a nutshell, DX is important, i spent weeks on getting this right. the nix community can learn a lot from the cargo way of logging!
it is very hard to use the current state of the @nix protocol and create a good DX with it. therefore i’ve created a few tickets which help to solve this:
in the libnix extension for cargo i’ve created the @cargo protocol, which is piggybacked onto @nix messages and extracted. this new @cargo protocol pretty much solves all the issues i addressed in the tickets above for the @nix protocol.
logone supports --level verbose
mode, there i tried to
map strings
to id
in order to map error
message(s) to respective mkDerivation(s), this is very desperate and
often fails. this needs to be fixed in nix.
below is the nix code which generates the @cargo error messages:
# generated from rustc-call.nix.handlebars using cargo (manual edits won't be persistent)
{ pkgs, fn, cargo, rustc, deps, build_parser, cargo-0_88_0-script_build-cfc654fccb259515 }: with deps;
pkgs.stdenv.mkDerivation rec {
name = "cargo-0_88_0-script_build_run-f5d51778f22880c0";
meta.cargo_crate_info = {
name = "cargo";
version = "0.88.0";
crate_hash = "f5d51778f22880c0";
};
buildInputs = [] ++ fn.inject meta.cargo_crate_info;
passthru.rust_crate_libraries = [];
passthru.rust_crate_parent = [cargo-0_88_0-script_build-cfc654fccb259515];
passthru.rust_script_build_run = [curl-sys-0_4_80_plus_curl-8_12_1-db5fbe1d9680c71f libgit2-sys-0_18_0_plus_1_9_0-86c4b3f8f5bf526a];
phases = "unpackPhase buildPhase";
src = builtins.filterSource
(path: type:
let base = baseNameOf path;
in !(base == "target" || base == "result" || builtins.match "result-*" base != null)
) /home/nixos/cargo;
unpackPhase = "";
RUSTC = "${rustc}/bin/rustc";
CARGO = "${cargo}/bin/cargo";
CARGO_CFG_FEATURE = "";
CARGO_CFG_PANIC = "unwind";
CARGO_CFG_TARGET_ABI = "";
CARGO_CFG_TARGET_ARCH = "x86_64";
CARGO_CFG_TARGET_ENDIAN = "little";
CARGO_CFG_TARGET_ENV = "gnu";
CARGO_CFG_TARGET_FAMILY = "unix";
CARGO_CFG_TARGET_FEATURE = "fxsr,sse,sse2";
CARGO_CFG_TARGET_HAS_ATOMIC = "16,32,64,8,ptr";
CARGO_CFG_TARGET_OS = "linux";
CARGO_CFG_TARGET_POINTER_WIDTH = "64";
CARGO_CFG_TARGET_VENDOR = "unknown";
CARGO_CFG_UNIX = "";
CARGO_ENCODED_RUSTFLAGS = "";
CARGO_MANIFEST_DIR = "./";
CARGO_MANIFEST_PATH = "./Cargo.toml";
CARGO_PKG_AUTHORS = "";
CARGO_PKG_DESCRIPTION = "Cargo, a package manager for Rust.";
CARGO_PKG_HOMEPAGE = "https://doc.rust-lang.org/cargo/index.html";
CARGO_PKG_LICENSE = "MIT OR Apache-2.0";
CARGO_PKG_LICENSE_FILE = "";
CARGO_PKG_NAME = "cargo";
CARGO_PKG_README = "README.md";
CARGO_PKG_REPOSITORY = "https://github.com/rust-lang/cargo";
CARGO_PKG_RUST_VERSION = "1.85";
CARGO_PKG_VERSION = "0.88.0";
CARGO_PKG_VERSION_MAJOR = "0";
CARGO_PKG_VERSION_MINOR = "88";
CARGO_PKG_VERSION_PATCH = "0";
CARGO_PKG_VERSION_PRE = "";
DEBUG = "true";
HOST = "x86_64-unknown-linux-gnu";
NUM_JOBS = "16";
OPT_LEVEL = "0";
PROFILE = "debug";
RUSTC_WORKSPACE_WRAPPER = "";
RUSTC_WRAPPER = "";
RUSTDOC = "rustdoc";
RUSTFLAGS = "";
TARGET = "x86_64-unknown-linux-gnu";
buildPhase = ''
export CARGO_MANIFEST_DIR=$(realpath $PWD/$CARGO_MANIFEST_DIR)
export CARGO_MANIFEST_PATH=$(realpath $PWD/$CARGO_MANIFEST_PATH)
mkdir -p $out/nix
export OUT_DIR=$out
export INC_DIR=$(${pkgs.mktemp}/bin/mktemp -d)
echo -e "\e[92mCompiling\e[0m cargo-0_88_0-script_build_run-f5d51778f22880c0"
echo "@cargo { \"type\":0, \"crate_name\":\"cargo\", \"id\":\"cargo-0_88_0-script_build_run-f5d51778f22880c0\" }"
for file in ${fn.environment_variables passthru.rust_script_build_run}; do
if [ -f $file ]; then
set -a
while read -r line; do
echo -e "\033[38;5;208m$line\033[0m"
done < "$file"
source $file
set +a
fi
done
build_script_build_output_lines=$(${pkgs.mktemp}/bin/mktemp)
set -x +e
${ cargo-0_88_0-script_build-cfc654fccb259515 }/build_script_build > $OUT_DIR/nix/build_script_build.out 2> $build_script_build_output_lines
build_script_build_exit_value=$?
set +x -e
if [ "$build_script_build_exit_value" -ne 0 ]; then
output=$(${pkgs.jq}/bin/jq -c -n \
--arg crate_name "cargo" \
--arg notice "There was an error executing build_script_build in file: '/home/nixos/cargo/target/debug/nix/derivations/cargo-0.88.0-script_build_run-f5d51778f22880c0.nix':" \
--arg exit_code "$build_script_build_exit_value" \
--rawfile msg $build_script_build_output_lines \
'{type: 3, crate_name: $crate_name, exit_code: ($exit_code|tonumber), messages: [ $notice, $msg ] }')
printf '@cargo %s\n' "$output"
cat $build_script_build_output_lines
exit $build_script_build_exit_value
fi
build_parser_output_lines=$(${pkgs.mktemp}/bin/mktemp)
set -x +e
${build_parser}/bin/cargo-build_script_build-parser $OUT_DIR/nix/build_script_build.out --out-path $out/nix write-results 2> $build_parser_output_lines
build_parser_exit_value=$?
set +x -e
if [ "$build_parser_exit_value" -ne 0 ]; then
output=$(${pkgs.jq}/bin/jq -c -n \
--arg crate_name "cargo" \
--arg notice "There was an error executing build_parser in file: '/home/nixos/cargo/target/debug/nix/derivations/cargo-0.88.0-script_build_run-f5d51778f22880c0.nix':" \
--arg exit_code "$build_parser_exit_value" \
--rawfile msg $build_parser_output_lines \
'{type: 3, crate_name: $crate_name, exit_code: ($exit_code|tonumber), messages: [ $notice, $msg ] }')
printf '@cargo %s\n' "$output"
cat $build_parser_output_lines
exit $build_parser_exit_value
fi
echo "@cargo {\"type\": 3, \"crate_name\": \"cargo\", \"exit_code\": 0, \"messages\": []}"
'';
}
traditionally a nix packager assumes that the software in question compiles and works and warnings during nix build are ignored it being c++ warnings, rustc warnings or other. as a result this leaves two types of error messages a nix packager is interested in:
one or more mkDerivation(s) failed:
what caused the problem? most often, missing system library or wrong version of the toolchains used.
a nix stack trace is only of little help.
nix programming error
we are interested in a nix stack trace.
nix logging seen from the cargo build
perspective:
with the advent of the libnix concept, shown in this cargo extension, we must adapt the logging:
summary
running this command below will create a build system in
target/debug/nix, call nix build
internally and symlink the
results at the end
for comparison: the build system to compile cargo itself, created by
cargo build
with the nix backend, is 2 Mib
uncompressed.
once the build system is in place, after running
cargo build
, one can evaluate it manually in order to
experiment with the implementation and to debug code:
nix build target --file /home/nixos/cargo/target/debug/nix/cargo_build_caller.nix --out-link /home/nixos/cargo/target/debug/nix/gc/result --json
this shows the root of the nix build system, the gc folder holds the symlink of the build so the result won’t be garbage-collected.
[nixos@nixos:~/cargo]$ ls -la target/debug/nix
total 20
drwxr-xr-x 4 nixos users 4096 Sep 20 01:16 .
drwxr-xr-x 8 nixos users 4096 Sep 22 08:33 ..
-rw-r--r-- 1 nixos users 1624 Sep 22 08:29 cargo_build_caller.nix
drwxr-xr-x 3 nixos users 4096 Sep 20 01:13 derivations
drwxr-xr-x 2 nixos users 4096 Sep 22 08:33 gc
derivations contains all the bins/libs which are no dependencies like
those added with cargo add serde
.
[nixos@nixos:~/cargo]$ ls -la target/debug/nix/derivations/
total 200
drwxr-xr-x 3 nixos users 4096 Sep 20 01:13 .
drwxr-xr-x 4 nixos users 4096 Sep 20 01:16 ..
-rw-r--r-- 1 nixos users 15626 Sep 22 08:29 cargo-0.88.0-27e7993d9cf32df7.nix
-rw-r--r-- 1 nixos users 15478 Sep 22 08:29 cargo-0.88.0-bin-85e09d7d8299b1ad.nix
-rw-r--r-- 1 nixos users 4706 Sep 22 08:29 cargo-0.88.0-script_build-cfc654fccb259515.nix
-rw-r--r-- 1 nixos users 5373 Sep 22 08:29 cargo-0.88.0-script_build_run-f5d51778f22880c0.nix
-rw-r--r-- 1 nixos users 5209 Sep 22 08:29 cargo-credential-0.4.8-a5adc6ab9fe103b0.nix
-rw-r--r-- 1 nixos users 5049 Sep 22 08:29 cargo-credential-libsecret-0.4.13-4e698a0b35f72d06.nix
-rw-r--r-- 1 nixos users 4496 Sep 22 08:29 cargo-platform-0.2.0-c5f768769f22a333.nix
-rw-r--r-- 1 nixos users 5918 Sep 22 08:29 cargo-util-0.2.20-7087e4a73afc7b23.nix
-rw-r--r-- 1 nixos users 5520 Sep 22 08:29 cargo-util-schemas-0.8.1-bce7b79eff35b46a.nix
-rw-r--r-- 1 nixos users 5132 Sep 22 08:29 crates-io-0.40.10-cb0425982b906266.nix
-rw-r--r-- 1 nixos users 47411 Sep 22 08:29 default.nix
drwxr-xr-x 2 nixos users 36864 Sep 20 01:13 deps
-rw-r--r-- 1 nixos users 4923 Sep 22 08:29 rustfix-0.9.0-9f1c66820d29e14a.nix
-rw-r--r-- 1 nixos users 276 Sep 22 08:29 target.nix
this folder contains all the crate dependencies considered read only.
[nixos@nixos:~/cargo]$ ls -la target/debug/nix/derivations/deps
total 3028
drwxr-xr-x 2 nixos users 36864 Sep 20 01:13 .
drwxr-xr-x 3 nixos users 4096 Sep 20 01:13 ..
-rw-r--r-- 1 nixos users 4010 Sep 22 08:29 adler2-2.0.0-115180b36279fc7c.nix
-rw-r--r-- 1 nixos users 5172 Sep 22 08:29 ahash-0.8.11-8f60023f209663c7.nix
-rw-r--r-- 1 nixos users 4313 Sep 22 08:29 ahash-0.8.11-script_build-dfb7cac56dd48573.nix
-rw-r--r-- 1 nixos users 5300 Sep 22 08:29 ahash-0.8.11-script_build_run-e66d9dc18aec0370.nix
-rw-r--r-- 1 nixos users 4283 Sep 22 08:29 aho-corasick-1.1.3-4faf1ab2f37c32c1.nix
-rw-r--r-- 1 nixos users 4208 Sep 22 08:29 allocator-api2-0.2.21-bd3713078dfee01f.nix
-rw-r--r-- 1 nixos users 7585 Sep 22 08:29 annotate-snippets-0.11.5-4d47bac9cbcd3256.nix
-rw-r--r-- 1 nixos users 8187 Sep 22 08:29 anstream-0.6.18-3af54164fe68ad61.nix
-rw-r--r-- 1 nixos users 7141 Sep 22 08:29 anstyle-1.0.10-bf6d032cb7d79be1.nix
-rw-r--r-- 1 nixos users 7360 Sep 22 08:29 anstyle-parse-0.2.6-7e3167a48452c319.nix
-rw-r--r-- 1 nixos users 7092 Sep 22 08:29 anstyle-query-1.1.2-df1354162236cfa0.nix
-rw-r--r-- 1 nixos users 4728 Sep 22 08:29 anyhow-1.0.96-139173be5e005a44.nix
....
this file is compiled into cargo as template and ships a reproducible toolchain using:
note: i tried to use flakes for this first but after days of trying i gave up and am now using simple nix files. main reaons were: flake.nix needs to sit at the project root and since this is a dynamic build system i can’t do that. also there might be a flake already.
the entry point into the build system:
[nixos@nixos:~/cargo]$ cat target/debug/nix/cargo_build_caller.nix
# generated from cargo_build_caller.nix.handlebars using cargo (manual edits won't be persistent)
{ system ? builtins.currentSystem }:
let
nixpkgsSrc = builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/25.05.tar.gz";
sha256 = "sha256:1915r28xc4znrh2vf4rrjnxldw2imysz819gzhk9qlrkqanmfsxd";
};
pkgs = import (nixpkgsSrc + "/pkgs/top-level/default.nix") {
localSystem = { inherit system; };
};
# nix-prefetch-git --url https://github.com/nixcloud/cargo-build_script_build-parser --branch-name main
build_parser_sources = pkgs.fetchFromGitHub {
owner = "nixcloud";
repo = "cargo-build_script_build-parser";
rev = "788a202b8646aae41761a70c8658878bdb8f0017";
sha256 = "1qi32hnkas9xfvwzjhy95vm7b0h8aj1fx1i9367qv1j8ng8hzcyn";
};
build_parser = pkgs.callPackage build_parser_sources {};
fenixSrc = pkgs.fetchFromGitHub {
owner = "nix-community";
repo = "fenix";
rev = "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42";
sha256 = "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=";
};
fenix = import fenixSrc {};
external_crate_dependencies =
(if builtins.pathExists ../../../Cargo.dependencies.nix
then builtins.trace "Using Cargo.dependencies.nix"
import ../../../Cargo.dependencies.nix { inherit pkgs; }
else builtins.trace "No Cargo.dependencies.nix found"
{ deps = {}; });
toolchain = fenix.stable.toolchain;
cargoPackages = import ./derivations/default.nix {
inherit pkgs external_crate_dependencies build_parser;
rustc = toolchain;
cargo = toolchain;
project_root = ./.;
};
in
cargoPackages
this file is optional and lives in the project root next to the
Cargo.toml
and helps with passing system libraries to
crates explicitly.
rust
while
using parts of the cargo
source code as a library.cargo2nix
creates a Crates.nix
while
crate2nix
creates a Cargo.nix
.i hate the resulting workflow as both need to be called explicitly after each Cargo.toml change and debugging is challenging once things start to fail.
note: these two tools focus more on the deployment side than on the development side when dealing with rust projects.
cargo2nix
,
crate2nix
and my cargo
+ nix backend want to
be.i see potential in these two projects because once nix is running on windows, we could refactor the build system created by cargo so it uses nix code from these projects to evalaute the project. sadly, right now this seems like science fiction. this is discussed in more detail in the next section.
note: these two tools focus more on the development side than on the deployment side when dealing with rust projects.
note: this concept clearly focus more on the deployment side and is very time consuming / hard to debug.
question: is calling
CARGO_BACKEND=nix cargo build
deterministic?
short answer: no, as it is written in
rust
and not nix
! thus we have IFD problems
when using it ‘on the fly’. however we could avoid IFD by shipping the
generated nix toolchain, at the cost of lots of additional files (127kb
bz2 compressed archive for cargo itself). this is similar to what we do
for c/cpp projects in nixpkgs. the main difference between the openssl
expression in nixpkgs and the crates here is that stuff in nixpkgs is
handcrafted and this is automated.
cargo rewrites like crane/naersk solve this at the cost of being detached from the upsteam cargo implementation often leading to unsupported edge-cases.
IMPORTANT, THE MAIN MOTIVATION MUST BE THIS:
the core compiler toolchain should be maintained by the cargo team and not as third party reimplementation of cargo logic! in the libnix rust workflow i focus on the ‘dev workflow’ and don’t want to rewrite the cargo source code from rust to nix.
long answer:
ideally the build system inputs are soly:
and the build system is implemented as nix code base and synthesized deterministically on the fly:
that would be the ideal goal! but this paradigm shift should be attempted after integrating nix into cargo and NOT BEFORE.
IMPORTANT, THE MAIN MOTIVATION MUST BE THIS:
one day, when nix is available on windows, only then it would make sense to port the ideas/concepts from crate2nix into the cargo nix backend and use that as a default.
it is very hard to compare the two build systems:
time cargo build
real 1m15.856s
time CARGO_BACKEND=nix ../cargo/target/debug/cargo build
real 2m31.875s
note:
nix-collect-garbage -d
removes
part of the toolchain.a key takeaway is, if you are a rust developer you are not interested in absolute build times but more in iterative build times and the current implementaion looses when there are many project crates as in the cargo example, as all of them have to be compiled each iteration hence no .fingerprint support and also the copying of the source code.
next steps involve:
cargo install
.fingerprint
conceptbwrap
so no source code copying is neededi would like to express my heartfelt thanks to:
--log-format internal-json
and other conceptsand to these useful services (no sponsors):
the libnix project was sponsored by the EU (and me).
this project presents a novel approach with the nix backend integrated into cargo to enhance the rust workflow. it offers a more native developer experience and leverages the strengths of nix. however, there are some challenges, such as the absence of .fingerprint support, leading to increased compilation time due to the necessity of copying store paths.
in the future, when nix becomes available on windows, we could make the code generator reproducible by utilizing a nix code base on all platforms, eliminating the need to distribute the toolchain for each project.
curiously, developing this backend took several months, posing a significant engineering challenge. nevertheless, thanks to the thoughtful design of cargo/rust by talented individuals, it was achievable.
if you’re interested in supporting this development, please reach out to me via x, linkedin, or email. refer to https://lastlog.de/blog/CurriculumVitae.html for further details.
your support is needed!