cargo-nix-plugin
A Nix plugin that resolves Cargo workspaces natively, replacing the generated
Cargo.nix file from crate2nix with a single builtins.resolveCargoWorkspace
primop.
What It Does
- Reads
Cargo.lockand the sparse registry index directly, so there is nocargobinary at eval time (you can also feed it pre-generatedcargo metadataJSON) - Evaluates
cfg()target expressions for your platform during resolution - Produces an attrset that plugs into
buildRustCrate - No more
crate2nix generatestep or checked-in 50K-100K lineCargo.nix
Install
Add the plugin to your Nix configuration:
# nix.conf or via --option — point at the directory so the right
# extension (.so/.dylib) is picked up automatically
plugin-files = /path/to/cargo-nix-plugin/lib/nix/pluginsOr use the flake output:
{
inputs.cargo-nix-plugin.url = "github:anthropics/cargo-nix-plugin";
}Usage
Default (lockfile resolve)
Point at your workspace root:
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.; # must contain Cargo.toml + Cargo.lock
};The plugin reads Cargo.lock plus the sparse registry index directly — no
cargo binary, no crate sources at eval time. On first use it fetches each
crate's index entry (a few hundred bytes) into $CARGO_HOME and reuses it
on later runs.
If you already redirect cargo to a mirror (CARGO_REGISTRIES_CRATES_IO_INDEX
or [source.crates-io] replace-with in .cargo/config.toml), the resolver
picks that up too:
# .cargo/config.toml — used by both cargo and the plugin
[source.crates-io]
replace-with = "mirror"
[source.mirror]
registry = "sparse+https://artifactory.example/api/cargo/crates/index/"If every index lookup fails (e.g. egress to index.crates.io is blocked and
no mirror is configured), evaluation fails rather than silently producing
derivations with missing features.
Explicit metadata
You can also generate cargo's resolution up front and pass it in:
cargo metadata --format-version 1 --locked > metadata.jsonThen:
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
metadata = builtins.readFile ./metadata.json;
cargoLock = builtins.readFile ./Cargo.lock;
src = ./.;
};Or use the helper:
nix run .#generate-metadata -- > metadata.jsonPre-fetching the index cache
If the machine doing the evaluation can't reach any index, run
cargo-nix-prefetch on one that can. It fills $CARGO_HOME and follows the
same mirror configuration as the plugin:
nix run .#cargo-nix-prefetch -- --manifest-path ./Cargo.toml
nix run .#cargo-nix-prefetch -- --manifest-path ./Cargo.toml --check # verifyUse --output DIR to write to a separate directory instead of $CARGO_HOME
and point the resolver at it:
nix run .#cargo-nix-prefetch -- --manifest-path ./Cargo.toml --output ./.cargo-indexcargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.;
cargoHome = ./.cargo-index; # pre-warmed by cargo-nix-prefetch
};You can also wrap this in a fixed-output derivation if you'd rather pin the cache by hash than check it in.
Git dependencies
git+… entries in Cargo.lock are fetched at eval time with
builtins.fetchGit { url; rev; allRefs = true; submodules = true; } so the
resolver can read each crate's Cargo.toml (the registry index has no
record of them). Submodules are pulled to match cargo, which always
recurses them for git deps. When the upstream repo is a Cargo workspace,
the resolver locates the right member and passes its sub-directory to
buildRustCrate as workspace_member.
Override gitSources when fetchGit can't reach the repo (private auth,
vendored fixture), to pin a narHash/use a FOD fetcher, or to skip
submodules for a repo that doesn't need them:
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.;
gitSources = {
# key = "${url}#${rev}" with git+ and ?query stripped — exactly what
# appears in Cargo.lock after `git+` and before `?`, plus `#REV`.
"https://github.com/Byron/gitoxide#abcdef…" = pkgs.fetchgit {
url = "git@github.com:Byron/gitoxide";
rev = "abcdef…";
hash = "sha256-…";
};
};
};A git+ source without a pinned #rev is rejected; Cargo.lock always
pins one.
Debug logging
The resolver is quiet by default. Set CARGO_NIX_DEBUG=1 to get
informational logs (mirror selection, index prefetch timings, per-crate
retries) on stderr. Warnings and errors are always printed.
Example
The plugin must be loaded by the same Nix version it was compiled against
(see Compatibility). Evaluate with the plugin loaded via
--option:
PLUGIN=$(nix build .#cargo-nix-plugin --print-out-paths)
NIX=$(nix build nixpkgs#nixVersions.nix_2_34 --print-out-paths | grep -v man)
$NIX/bin/nix-instantiate --eval \
--option plugin-files "$PLUGIN/lib/nix/plugins" \
-E '(import ./lib { pkgs = import <nixpkgs> {}; src = ./.; }).workspaceMembers'Or permanently in nix.conf / ~/.config/nix/nix.conf (only if your system
Nix matches the plugin's build version):
plugin-files = /path/to/cargo-nix-plugin/lib/nix/pluginsflake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
cargo-nix-plugin.url = "github:anthropics/cargo-nix-plugin";
};
outputs = { self, nixpkgs, cargo-nix-plugin }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.;
};
in {
packages.x86_64-linux.default = cargoNix.rootCrate.build;
};
}Clippy
The wrapper provides cached clippy checks via cargoNix.clippy. Dependencies
are compiled once with rustc and cached in the Nix store; only workspace
members are re-checked with clippy-driver. This means running clippy on a
large workspace is as fast as compiling just your local crates.
Pass clippyAllFeatures = true (lockfile-resolve mode only) to lint with
every workspace feature enabled, like cargo clippy --all-features, so
feature-gated code is checked too.
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.;
};
# Check all workspace members
cargoNix.clippy.allWorkspaceMembers
# Check a single member
cargoNix.clippy.workspaceMembers.my-crate.build
# Collect cached JSON diagnostics from all workspace members
cargoNix.clippy.reportTo fail on warnings, pass extra clippy flags:
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.;
clippyArgs = [ "-D" "warnings" ];
};Workspace members are built with --cap-lints forbid (no effective cap,
matching cargo). Pass clippyCapLints = "warn" to demote all findings to
warnings instead.
Each clippy member build retains its raw rustc JSON diagnostics at
cargoNix.clippy.workspaceMembers.<name>.report. The aggregate
cargoNix.clippy.report derivation collects those cached diagnostics as one
<name>.jsonl file per workspace member, so CI can publish annotations without
re-running clippy. The build log still shows the rendered diagnostics; only the
on-disk report is JSON.
The report is an output of the clippy build, so it only exists when that build
succeeds. If you need both the JSON and a failing CI gate, leave clippyArgs
without -D warnings and build cargoNix.clippy.reportCheck instead — it
depends on clippy.report and fails if any member has warning- or error-level
findings.
How clippy caching works
clippy-driver is a drop-in replacement for rustc — it accepts identical
command-line flags and produces the same artifacts, but also runs lint passes.
The wrapper creates a small shim package where bin/rustc calls
clippy-driver, and passes it as the rust override to buildRustCrate for
workspace members only. Non-workspace dependencies use the normal rustc and
resolve to the exact same Nix store paths as a regular build — no redundant
compilation. With clippyAllFeatures = true, dependencies whose feature set
grows are rebuilt once for the clippy path; everything else is still shared
with the regular build.
Tests
checks.x86_64-linux.my-crate-tests =
cargoNix.workspaceMembers.my-crate.runTests;runTests compiles lib unit tests and integration tests under tests/
(with [dev-dependencies] wired in) and runs them sequentially. The regular
.build derivation is unchanged. Integration tests can spawn the crate's
binaries via env!("CARGO_BIN_EXE_<name>") exactly as under cargo test.
Tests that shell out to external tools at runtime declare them via
nativeCheckInputs in crateOverrides; runTests puts them on PATH:
cargoNix = cargo-nix-plugin.lib {
inherit pkgs;
src = ./.;
crateOverrides = pkgs.defaultCrateOverrides // {
my-crate = _: { nativeCheckInputs = [ pkgs.sqlite ]; };
};
};The runner sets RUST_BACKTRACE=1 and points CARGO_TARGET_TMPDIR at a
fresh temp dir. If you need different behaviour (test filters, --nocapture,
a custom harness), the compiled artefacts are at .buildTests —
$out/tests/* are the test executables, $out/bin/* the real binaries —
and runTests.passthru.testsDrv points there too.
Known limitations: doctests are not built, per-[[bin]] unit tests are not
compiled, and tests under examples/ / benches/ are not discovered.
How It Works
-
Nix plugin: Adds a
builtins.resolveCargoWorkspaceprimop to Nix. When you callcargo-nix-plugin.lib { ... }, this primop resolves your entire Cargo workspace — dependencies, features, platform-specific conditionals — and returns the crate graph as a Nix attrset. In the default mode it readsCargo.lockand the sparse registry index directly; in explicit mode it parses pre-providedcargo metadataJSON. -
Nix wrapper: Takes the resolved crate graph and builds each crate with
buildRustCrate, wiring up dependencies automatically. Supports proc-macro cross-compilation, crate overrides, and the standardworkspaceMembers/rootCrateinterface.
Target Platform
The plugin accepts a target description attrset:
target = {
name = "x86_64-unknown-linux-gnu";
os = "linux"; arch = "x86_64"; vendor = "unknown"; env = "gnu";
family = ["unix"]; pointer_width = "64"; endian = "little";
unix = true; windows = false;
};The wrapper auto-detects this from stdenv.hostPlatform.
Custom cfgs
To set custom cfgs during [target.'cfg(...)'] dependency resolution
(equivalent to RUSTFLAGS="--cfg foo" at cargo-metadata time), pass
extraCfgs:
extraCfgs = [ "my_platform" ];Pair with passing the same --cfg via rustc opts so #[cfg(foo)] in source
compiles too — extraCfgs only affects dependency resolution.
Compatibility
-
Nix: The plugin must be loaded by the same Nix version it was compiled against — the Nix plugin ABI is not stable across versions. If you see errors like
expected a set but found a set, you have a version mismatch..#cargo-nix-plugin(the default) is built against Nix 2.34, so use Nix 2.34.x to evaluate:# Get the matching nix NIX=$(nix build nixpkgs#nixVersions.nix_2_34 --print-out-paths | grep -v man) PLUGIN=$(nix build .#cargo-nix-plugin --print-out-paths) $NIX/bin/nix build .#myPackage \ --option plugin-files "$PLUGIN/lib/nix/plugins"For other Nix versions, build the matching per-version attribute, e.g.
.#cargo-nix-plugin-nix_2_31to pair withnixVersions.nix_2_31. The flake'snixVersionsset (inflake.nix) lists what's currently built; Nix >= 2.30 is required. -
Platforms:
x86_64-linux,aarch64-linux, andaarch64-darwin. Cross-compilation to other target platforms is supported. -
API level:
lib/checks that the loaded plugin speaks the same contract version before resolving and warns on mismatch (e.g. when the plugin baked into your Nix lags thelib/checkout). The wrapper result exposes both sides so you can turn that into a hard failure:let cargoNix = import ./lib { inherit pkgs; src = ./.; }; in assert cargoNix.apiLevel == cargoNix.resolverApiLevel; cargoNix.workspaceMembersapiLevelis what thislib/speaks;resolverApiLevelis what the loaded plugin reports (0 if the plugin predates the check). -
buildRustCrate: Compatible with nixpkgs
buildRustCrateanddefaultCrateOverrides
Status
Maintained by Anthropic. Provided AS IS without warranty (see LICENSE).
We triage issues and review pull requests but do not commit to fixing every
bug or accepting every feature request. For security issues, see
SECURITY.md.
License
Apache License 2.0. See LICENSE.