Per-crate Nix builds from Cargo's unit graph.
On a 457-crate workspace:
| What changed | unit2nix rebuilds | Crane rebuilds |
|---|---|---|
| Edit a local crate | 31 | 457 |
Bump serde |
133 | 457 |
Bump tokio |
70 | 457 |
| Bump a leaf dep | 41 | 457 |
cargo build --unit-graph ─┐
cargo metadata ───────────┼─→ unit2nix ─→ build-plan.json
Cargo.lock checksums ─────┤
nix-prefetch-git ─────────┘ (git deps only)
The Nix side reads the JSON and calls buildRustCrate for each crate.
nix flake init -t github:brittonr/unit2nix
nix run github:brittonr/unit2nix
nix buildThe build plan embeds a Cargo.lock hash. If Cargo.toml or Cargo.lock changes, re-run unit2nix — Nix eval fails on hash mismatch.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
unit2nix.url = "github:brittonr/unit2nix";
};
outputs = { nixpkgs, unit2nix, ... }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
ws = unit2nix.lib.x86_64-linux.buildFromUnitGraph {
inherit pkgs;
src = ./.;
resolvedJson = ./build-plan.json;
};
in {
packages.x86_64-linux.default = ws.workspaceMembers."my-crate".build;
};
}ws = unit2nix.lib.x86_64-linux.buildFromUnitGraphAuto {
inherit pkgs;
src = ./.;
};Plan is generated at eval time via import-from-derivation. Works everywhere except Hydra.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
unit2nix.url = "github:brittonr/unit2nix";
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.unit2nix.flakeModules.default ];
systems = [ "x86_64-linux" "aarch64-linux" ];
unit2nix = {
enable = true;
src = ./.;
resolvedJson = ./build-plan.json; # null for auto mode
defaultPackage = "my-bin";
};
};
}pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ unit2nix.overlays.default ];
};
ws = pkgs.unit2nix.buildFromUnitGraph {
src = ./.;
resolvedJson = ./build-plan.json;
};Both buildFromUnitGraph and buildFromUnitGraphAuto return:
| Attribute | Description |
|---|---|
workspaceMembers.<name>.build |
Built workspace member |
rootCrate.build |
Root package (single-package projects) |
allWorkspaceMembers |
symlinkJoin of all members |
test.check.<name> |
#[test] runner (requires --include-dev or --workspace) |
clippy.allWorkspaceMembers |
Clippy all members |
builtCrates.crates.<pkgId> |
Every crate derivation by package ID |
ring, openssl-sys, libgit2-sys, tikv-jemalloc-sys, and other common -sys crates are handled by built-in overrides plus nixpkgs' defaultCrateOverrides. Run unit2nix --check-overrides to see what's covered.
For project-specific crates:
buildFromUnitGraph {
inherit pkgs src;
resolvedJson = ./build-plan.json;
extraCrateOverrides = {
my-custom-sys = attrs: {
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.some-library ];
};
};
}Cargo resolves different dep trees per target, so each target gets its own plan:
unit2nix --target aarch64-unknown-linux-gnu -o build-plan-aarch64.jsonbuildFromUnitGraph {
pkgs = pkgs.pkgsCross.aarch64-multiplatform;
src = ./.;
resolvedJson = ./build-plan-aarch64.json;
}For workspaces, use --workspace to capture dev-dependencies for all members:
cargo unit2nix --workspace
nix build .#test.check.my-crateFor single-crate projects, --include-dev is sufficient:
cargo unit2nix --include-dev
nix build .#test.check.my-crateGit deps are prefetched at generation time so builds stay pure. In auto mode, place a crate-hashes.json at the root (same format as crate2nix):
{
"https://github.com/user/repo?branch=main#crate-name@1.0.0": "sha256-..."
}unit2nix [OPTIONS]
--manifest-path <PATH> Path to Cargo.toml [default: ./Cargo.toml]
--features <FEATURES> Comma-separated features to enable
--all-features Enable all features
--no-default-features Disable default features
--bin <NAME> Build a specific binary target
-p, --package <NAME> Build a specific package
--members <NAMES> Workspace members to include (comma-separated)
--target <TRIPLE> Cross-compilation target
--include-dev Include dev-dependencies (for nix test support)
--workspace Resolve all workspace members + dev-deps (implies --include-dev)
-o, --output <FILE> Output file [default: build-plan.json]
--stdout Write to stdout
--force Regenerate even if inputs haven't changed
--check-overrides Report -sys crate override coverage
--json Machine-readable output (with --check-overrides)
--no-check Skip override check after generation
Also available as cargo unit2nix.
| unit2nix | crate2nix | |
|---|---|---|
| Resolver | Cargo itself (unit graph) | Reimplemented in Rust |
| Platform filtering | Cargo does it | cfg() evaluator in Nix |
| Cross-compilation | One JSON per target | One JSON, filtered at eval |
| Stability | Nightly (needs --unit-graph) |
Stable Rust |
unit2nix requires nightly Cargo for --unit-graph.
| Project | Crates | Notes |
|---|---|---|
| ripgrep | 34 | Pure Rust, zero overrides |
| fd | 59 | jemalloc covered by built-ins |
| bat | 168 | libgit2, libz, onig auto-covered |
| nushell | 519 | sqlite, ring auto-covered |
| Private workspace | 457 | Production build |
cargo test # unit tests
nix flake check # sample builds, clippy, overlay/module smoke tests,
# override coverage, fd/bat/ripgrep/nushell validation,
# cross-compilation, NixOS VM integration tests- Nightly Rust (
--unit-graphis unstable) - Nix with flakes
MIT