Rust projects are generally managed with Cargo, but it has the disadvantage of having to recompile all dependencies once per project, taking up a lot of hard disk space, and not being able to share the build cache across projects. After some research, there are several Nix-based Rust build tools:

I’ll try out each of these tools below.

Some of the commands that appear below are referenced in the documentation for the corresponding project.

cargo2nix

  • Development Shell - knowing all the dependencies means easy creation of complete shells. Run nix develop or direnv allow in this repo and see!
  • Caching - CI & CD pipelines move faster when purity guarantees allow skipping more work!
  • Reproducibility - Pure builds. Access to all of nixpkgs for repeatable environment setup across multiple distributions and platforms

Installation

cargo2nix provides flakes support and does not need to be installed separately.

Usage

cargo2nix is relatively easy to run, just run nix run directly using the flakes feature.

1
nix run github:cargo2nix/cargo2nix

It generates a Cargo.nix file, and you need to write a flake.nix to use with it, here is jiegec/webhookd as an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  inputs = {
    cargo2nix.url = "github:cargo2nix/cargo2nix/release-0.11.0";
    flake-utils.follows = "cargo2nix/flake-utils";
    nixpkgs.follows = "cargo2nix/nixpkgs";
  };

  outputs = inputs: with inputs;
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          overlays = [cargo2nix.overlays.default];
        };

        rustPkgs = pkgs.rustBuilder.makePackageSet {
          rustVersion = "1.61.0";
          packageFun = import ./Cargo.nix;
        };

      in rec {
        packages = {
          webhookd = (rustPkgs.workspace.webhookd {}).bin;
          default = packages.webhookd;
        };
      }
    );
}

Then compile.

1
2
3
4
$ git add .
$ nix build
$ ./result-bin/bin/webhookd --version
webhookd 0.2.1

Principle

cargo2nix parses Cargo.lock, generates Cargo.nix file, and finally wraps it into flake.nix.

crane

  • Source fetching: automatically done using a Cargo.lock file
  • Incremental: build your workspace dependencies just once, then quickly lint, build, and test changes to your project without slowing down
  • Composable: split builds and tests into granular steps. Gate CI without burdening downstream consumers building from source.

Installation

crane does not need to be installed, just use flakes.

Usage

When using crane, write flake.nix directly in the project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    crane.url = "github:ipetkov/crane";
    crane.inputs.nixpkgs.follows = "nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, crane, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system: {
      packages.default = crane.lib.${system}.buildPackage {
        src = ./.;
      };
    });
}

This works without the need to use the tool to generate the corresponding Cargo.nix from Cargo.lock.

However, since webhookd relies on the native library, you will need to add the native dependencies manually.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    crane.url = "github:ipetkov/crane";
    crane.inputs.nixpkgs.follows = "nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, crane, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
        };
      in
      {
        packages.default = crane.lib.${system}.buildPackage {
          src = ./.;

          buildInputs = with pkgs; [
            libiconv
            darwin.apple_sdk.frameworks.Security
          ];
        };
      });
}

Build.

1
2
3
$ nix build
$ ./result/bin/webhookd --version
webhookd 0.2.1

Principle

crane does an incremental compilation by downloading all the dependencies and building them once with cargo, and then building the resulting target directory as target.tar.zst, and then adding the project source code again. However, its purpose is not quite the same as other projects, and it does not take into account cross-project dependency caching.

crate2nix

  • Same dependency tree as cargo: It uses cargo_metadata to obtain the dependency tree from cargo. Therefore, it will use the exact same library versions as cargo and respect any locked down version in Cargo.lock.
  • Smart caching: It uses smart crate by crate caching so that nix rebuilds exactly the crates that need to be rebuilt. Compare that to docker layers…
  • Nix ecosystem goodness: You can use all things that make the nix/NixOS ecosystem great, e.g. distributed/remote builds, build minimal docker images, deploy your binary as a service to the cloud with NixOps, …
  • Out of the box support for libraries with non-rust dependencies: It builds on top of the buildRustCrate function from NixOS so that native dependencies of many rust libraries are already correctly fetched when needed. If your library with native dependencies is not yet supported, you can customize defaultCrateOverrides / crateOverrides, see below.
  • Easy to understand nix template: The actual nix code is generated via templates/build.nix.tera so you can fix/improve the nix code without knowing rust if all the data is already there.

Installation

First install crate2nix, since its stable version 0.10.0 is already last year’s version, I directly used the master branch. If you are installing directly, you can use the following command.

1
nix-env -i -f ht

But I am managing it with flakes + home-manager, so here is how I actually configured it.

  1. add crate2nix to flake.nix to inputs and set crate2nix.flake = false
  2. pass crate2nix from inputs to the actual home manager configuration, then add callPackage crate2nix {} to home.packages

Usage

Next, find a Rust project and run crate2nix generate in it.

1
2
$ crate2nix generate
Generated ./Cargo.nix successfully.

Build.

1
nix build -f Cargo.nix rootCrate.build

The result of the compilation can be seen under result/bin.

I compiled jiegec/webhookd and got an error during the compilation.

1
2
3
4
5
>   = note: ld: framework not found Security
>           clang-11: error: linker command failed with exit code 1 (use -v to see invocation)
>
>
> error: aborting due to previous error

The previous cargo2nix did not have this problem, supposedly because cargo2nix introduced the Security dependency for us, see overrides.nix.

According to the crate2nix documentation, additional native dependencies need to be added.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{ pkgs ? import <nixpkgs> { } }:

let
  generatedBuild = import ./Cargo.nix {
    inherit pkgs;
    defaultCrateOverrides = with pkgs; defaultCrateOverrides // {
      webhookd = attrs: {
        buildInputs =
          lib.optionals
            stdenv.isDarwin
            [ darwin.apple_sdk.frameworks.Security ];
      };
    };
  };
in
generatedBuild.rootCrate.build

Then build.

1
2
3
$ nix build -f default.nix
$ ./result/bin/webhookd --version
webhookd 0.2.1

This will work fine.

Principle

The principle is to use the cargo_metadata library to get the information of each crate from Cargo.lock and then translate it to Cargo.nix, after which nix compiles the contents of each crate. So at first, you still need to create a project with Cargo, add dependencies, and generate Cargo.lock; then you can use crate2nix generate to synchronize the dependency information to the Cargo.nix file, and build it without Cargo’s participation, directly rustc.

naersk

Installation

Requires installation of niv:

1
nix-env -iA nixpkgs.niv

Usage

In the project directory, first import naersk with niv.

1
2
niv init
niv add nix-community/naersk

Then write a default.nix.

1
2
3
4
5
6
let
  pkgs = import <nixpkgs> { };
  sources = import ./nix/sources.nix;
  naersk = pkgs.callPackage sources.naersk { };
in
naersk.buildPackage ./.

Build.

1
2
3
$ nix build -f default.nix
$ ./result/bin/webhookd --version
webhookd 0.2.1

Principle

The principle of naersk is similar to crane: download all the dependencies, create a project with only dependencies, then precompile with cargo, compile the target directory into target.tar.zst; then compile the whole project based on the precompiled results.

nocargo

  • No IFDs (import-from-derivation). See meme.
  • No cargo dependency during building. Only rustc.
  • No need for hash prefetching or code generation1.
  • Crate level caching, globally shared.
  • nixpkgs integration for non-Rust dependencies.

The README also mentions a comparison of nocargo, cargo2nix, naersk and buildRustPackage.

Usage

nocargo is currently only supported on x86_64-linux platforms.

In a Cargo project, run the following command.

1
nix run github:oxalica/nocargo init

It generates the flake.nix file as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# See more usages of nocargo at https://github.com/oxalica/nocargo#readme
{
  description = "Rust package webhookd";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
    nocargo = {
      url = "github:oxalica/nocargo";
      inputs.nixpkgs.follows = "nixpkgs";
      # inputs.registry-crates-io.follows = "registry-crates-io";
    };
    # Optionally, you can override crates.io index to get cutting-edge packages.
    # registry-crates-io = { url = "github:rust-lang/crates.io-index"; flake = false; };
  };

  outputs = { nixpkgs, flake-utils, nocargo, ... }@inputs:
    flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
      let
        ws = nocargo.lib.${system}.mkRustPackageOrWorkspace {
          src = ./.;
        };
      in rec {
        packages = {
          default = packages.webhookd;
          webhookd = ws.release.webhookd.bin;
          webhookd-dev = ws.dev.webhookd.bin;
        };
      });
}

Build.

1
2
$ git add .
$ nix build

A compile error occurs, indicating that the crates.io index version is not up to date.

1
2
error: Package bytes doesn't have version 1.2.0 in index. Available versions: 0.0.1 0.1.0 0.1.1 0.1.2 0.2.0 0.2.1 0.2.10 0.2.11 0.2.2 0.2.3 0.2.4 0.2.5 0.2.6 0.2.7 0.2.8 0.2.9 0.3.0 0.4.0 0.4.1 0.4.10 0.4.11 0.4.12 0.4.2 0.4.3 0.4.4 0.4.5 0.4.6 0.4.7 0.4.8 0.4.9 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.6.0 1.0.0 1.0.1 1.1.0
(use '--show-trace' to show detailed location information)

Follow the instructions in flake.nix to use the latest crates.io index.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# See more usages of nocargo at https://github.com/oxalica/nocargo#readme
{
  description = "Rust package webhookd";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
    nocargo = {
      url = "github:oxalica/nocargo";
      inputs.nixpkgs.follows = "nixpkgs";
      inputs.registry-crates-io.follows = "registry-crates-io";
    };
    # Optionally, you can override crates.io index to get cutting-edge packages.
    registry-crates-io = { url = "github:rust-lang/crates.io-index"; flake = false; };
  };

  outputs = { nixpkgs, flake-utils, nocargo, ... }@inputs:
    flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
      let
        ws = nocargo.lib.${system}.mkRustPackageOrWorkspace {
          src = ./.;
        };
      in rec {
        packages = {
          default = packages.webhookd;
          webhookd = ws.release.webhookd.bin;
          webhookd-dev = ws.dev.webhookd.bin;
        };
      });
}

Continue building and you’re done.

1
2
3
4
5
6
7
8
$ nix build
• Updated input 'nocargo/registry-crates-io':
    'github:rust-lang/crates.io-index/1ce12a7e3367a2a673f91f07ab7cc505a0b8f069' (2022-07-17)
  → follows 'registry-crates-io'
• Added input 'registry-crates-io':
    'github:rust-lang/crates.io-index/627caba32f416e706bf3f2ceac55230ec79710c5' (2022-08-02)
$ ./result/bin/webhookd --version
webhookd 0.2.1

Summary

As you can see, the different tools above use different approaches, if you want to compare.

  • Nix drv granularity: per dependency (cargo2nix, crate2nix, nocargo), all dependencies (crane, naersk). The advantage of the former is that the dependencies will be shared across projects and further can be passed to the binary cache.
  • Whether to generate nix files with full dependency information: yes (cargo2nix, crate2nix), no (crane, naersk, nocargo). If generated, the information in Cargo.lock and Cargo.nix in the repository is duplicated, so if Cargo.lock is modified, you need to resync Cargo.nix.