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
buildRustPackage | import-cargo | naersk | crate2nix | |
---|---|---|---|---|
Requires Generated Files | No | No | No | Yes |
Manual FOD hashes | Yes | No | No | No |
Requires experimental Nix | No | Yes (but see comment below) | No | No |
Incremental build | No | No | Yes, but not for dependencies | Yes, for all crates |