What options are there to build Rust projects with Nix?

This post is for those that are already convinced that they want to use Nix for building their Rust applications. Over the course of the last years a few different approaches to the problem have evolved. Each comes with up- & downsides and the right choice depends on your requirements. I most likely forgot one or more tools that I haven’t used. Feel free to shoot me an email and I’ll do my best of adding it.

buildRustPackage

Within Nixpkgs there is a builder for Rust packages called buildRustPackage.There is not much secret sauce in the idea. The build is basically split into to phases. The first phase downloads all the dependencies using cargo-download. The second phase invokes cargo to build & optionally run the tests.

Here is how you use it:

{ nixpkgs ? <nixpkgs> }:
let pkgs = import nixpkgs {}; in
pkgs.buildRustPackage {
  pname = "my-package";
  version = "1.0";
  src = ./.;
  cargoSha256 = "0000000000000000000000000000000000000000000000000000000000000000";
}

As you can see the usage is really straight forward and you do not need to generate any files in the repository. That being said you’ll have to update the cargoSha256 field whenever you change any of your dependencies.

The obvious downside is that fixed output derivation can be hard to deal with and this usage is, to some degree, an abuse of the FOD’s that were intended for simple fetching of source archives. In the past buildRustPackage builds had issues across difference architectures as cargo-vendor would generate different outputs.

Pro:

  • Really low bar for entry
  • No generated files to check into the repo

Con:

  • Slow for big projects as it compiles everything in one go.
  • Fixed output hashes have to be maintained.

import-cargo

Eelco Dolstra wrote cargo-import which, compared to the previous contender, doesn’t require any fixed output updating or manual setps after the initial setup. Just like buildRustPackage, it is doing all the compilation as a single large build without any support for incremental recompilation.

The usage, as described in the repo, requires you to use the experimental flakes support in nix:

{
  description = "My Rust project";

  inputs = {
    nixpkgs.url = github:NixOS/nixpkgs/nixos-20.03;
    import-cargo.url = github:edolstra/import-cargo;
  };

  outputs = { self, nixpkgs, import-cargo }: let

    inherit (import-cargo.builders) importCargo;

  in {

    defaultPackage.x86_64-linux =
      with import nixpkgs { system = "x86_64-linux"; };
      stdenv.mkDerivation {
        name = "testrust";
        src = self;

        nativeBuildInputs = [
          # setupHook which makes sure that a CARGO_HOME with vendored dependencies
          # exists
          (importCargo { lockFile = ./Cargo.lock; inherit pkgs; }).cargoHome

          # Build-time dependencies
          rustc cargo
        ];

        buildPhase = ''
          cargo build --release --offline
        '';

        installPhase = ''
          install -Dm775 ./target/release/testrust $out/bin/testrust
        '';

      };

  };
}

(example taken from the upstream repo)

For anyone that is familiar with regular stdenv.mkDerivation builds this should read very familiar. You can integrate this easily into poly-language projects that might invoke cargo at some point during the build.

These days I still actively avoid experimental flakes for most of my projects. I’ve extracted the logic into a Gist that you can just callPackage.

Pro:

  • No generated files to check into the repo
  • No manual updating of files even when dependencies change

Cons:

  • Upstream version requires usage of experimental Nix features
  • Slow for big projects as it compiles everything in one go.

naersk

naersk is very similar to import-cargo but it has a major upside: It builds dependencies independent from the actual target. This gives you caching of dependencies and reduces the main build to just building your code and linking it to the dependencies.

While it is doing much better than any of the alternatives already it still isn’t perfect for large repositories with multiple rust projects that share a large number of overlapping dependencies. I’m not holding this against it. It is an excellent piece nonetheless.

Instead of quoting a usage example I’d like to refer you to the upstream documentation which is really well done and offers more options than I would consider covering here.

Pro:

  • Incremental builds with cached dependencies
  • No generated files to check into the repo
  • No manual updating of files even when dependencies change

Cons: Not really any unless you have plenty of in-house crates that share dependencies (or depend on each other).

crate2nix

Last but not least, there is also crate2nix, which is the logical replacement of carnix (an early pioneer in the era that is effectively dead). Back in the days carnix introduced buildRustCrate to nixpkgs. The main motivation here is to build each crate as their own derivation. This leads to very good incremental builds. You will rebuild a sub-tree of the dependencies when you change one instead of all of them. It also allows you to re-use already compiled dependencies across rust builds which furthermore cuts down the compile times in some situations.

The downside to all the other approaches is that you’ll have to generate Nix expressions and check them into your repository. These expressions can be quite large but for some the benefits might outweigh the costs.

In order to use it you’ll have to make the crate2nix tool available in your shell. Once you’ve done that it comes down to invoking crate2nix generate followed by nix-build -A rootCrate.build.

Pro:

  • Suitable for large projects where build closures are overlapping as each dependency is it’s own derivation

Cons:

  • Large generated files have to be checked into the repo

Comparison table

buildRustPackageimport-cargonaerskcrate2nix
Requires Generated FilesNoNoNoYes
Manual FOD hashesYesNoNoNo
Requires experimental NixNoYes (but see comment below)NoNo
Incremental buildNoNoYes, but not for dependenciesYes, for all crates