Nix (PDF) provides users a way to access the massive Nixpkgs library of packages, create reproducable builds of software, roll slim containers, create declarative VMs, or run their whole machines. A new feature of Nix, Flakes, is bringing a convention to how projects like Rust crates can be accessed, integrated, and used within Nix (or NixOS.)
Let's explore how we can make our Rust crate usable as a Nix flake. At the end of this, any nix user with Flakes enabled should be able to run your project with something like nix run github:user/project. They'll be able to add your repository as a Nix overlay, install the package, do interactive builds, or create a portable bundle of it.
Prerequisites
Before we get started, you'll need a Nix with the flakes feature enabled. Let's do that!
If using NixOS:
{
# Use edge NixOS.
nix.extraOptions = ''
experimental-features = nix-command flakes
'';
nix.package = pkgs.nixUnstable;
nixpkgs.config.allowUnfree = true;
}
Then do a nixos-rebuild switch to activate it. You may also want to enable nix-direnv.
If using Nix
You may also want to enable nix-direnv.
Setup
For this article, I'll create a new crate. You can use your existing crate! You should also ensure it's a Git repository. (Flakes require this!)
cargo init scratch
cd scratch
nvim Cargo.toml
// Add a `description = "boop"` field under `[package]`!
git init
git add src/ Cargo.toml
flake.nix
Next, we'll create a flake.nix. This is the coarse equivalent of a Cargo.toml file in Rust. (In fact, later we'll even see a flake.lock, just like Cargo!)
Flakes are a Nix file with a predetermined schema. The wiki page includes a schema.
Our flake will have three items in the root:
description: A string description of the flake. Consider making this your crate description!inputs: The inputs required by the flake. Generallynixpkgsandnaersk(for the fancy Rust builder) will be enough.outputs: A function accepting the (realized) inputs, returning the outputs of the flake, such as packages, NixOS modules, or library items!
The first two are pretty self explanatory, let's dig into outputs! We only use a few of the available options:
overlay: A Nix overlay which can have package definitions added. Can be added to Nix systems to allow adding items to the environment (such as viaenvironment.systemPackages.)packages: A set of packages one could invoke withnix run $FLAKE#packageNameor build withnix build $FLAKE#packageName.defaultPackage: The default item to run/build whennix run $FLAKEornix build $FLAKEis invoked.checks: A set of checks (think likecargo fmt) to run whenevernix flake checkis run. This is particularly useful for pre-commit hooks or CI.devShell: An environment for whennix developis run. This is also used innix-direnv.
You shouldn't need to make many changes to this file, but you should customize the description and remove any platforms you don't support.
# flake.nix
{
description = "My cute Rust crate!";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
naersk.url = "github:nmattia/naersk";
naersk.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, naersk }:
let
cargoToml = (builtins.fromTOML (builtins.readFile ./Cargo.toml));
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
in
{
overlay = final: prev: {
"cargoToml.package.name" = final.callPackage ./. { inherit naersk; };
};
packages = forAllSystems (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [
self.overlay
];
};
in
{
"cargoToml.package.name" = pkgs."cargoToml.package.name";
});
defaultPackage = forAllSystems (system: (import nixpkgs {
inherit system;
overlays = [ self.overlay ];
})."cargoToml.package.name");
checks = forAllSystems (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [
self.overlay
];
};
in
{
format = pkgs.runCommand "check-format"
{
buildInputs = with pkgs; [ rustfmt cargo ];
} ''
pkgs.rustfmt/bin/cargo-fmt fmt --manifest-path ././Cargo.toml -- --check
pkgs.nixpkgs-fmt/bin/nixpkgs-fmt --check ./.
touch $out # it worked!
'';
"cargoToml.package.name" = pkgs."cargoToml.package.name";
});
devShell = forAllSystems (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ self.overlay ];
};
in
pkgs.mkShell {
inputsFrom = with pkgs; [
pkgs."cargoToml.package.name"
];
buildInputs = with pkgs; [
rustfmt
nixpkgs-fmt
];
LIBCLANG_PATH = "pkgs.llvmPackages.libclang.lib/lib";
});
};
}
Then git add flake.nix.
default.nix
This file is specific to our crate! It is a Nix package. Let's break it down, top to bottom:
- The Inputs (
{ the, stuff, here }:): This list of function arguments represents all of the inputs into the expression. - A
let ... inbinding: Here we set a variable which when accessed evaluates theCargo.toml. - A
naerskcall:naerskis an improvement over thenixpkgs.buildRustPackagetraditionally available. - A
srclocation: This is the location of your code! buildInputs: A list of dependencies your crate needs at build time.checkInputs: A list of dependencies for your crate at test time.doCheck: If Nix should run checks (cargo test).CARGO_BUILD_INCREMENTALandRUST_BACKTRACE: Environment variables we opt to set.copyLibs: If Nix should copy over built libraries as well.nameandversion: Derived from yourCargo.toml.meta: Some metadata about the package. Optionally you can setmainProgram = "thing"here.
# default.nix
{ lib
, naersk
, stdenv
, clangStdenv
, hostPlatform
, targetPlatform
, pkg-config
, libiconv
, rustfmt
, cargo
, rustc
# , llvmPackages # Optional
# , protobuf # Optional
}:
let
cargoToml = (builtins.fromTOML (builtins.readFile ./Cargo.toml));
in
naersk.lib."targetPlatform.system".buildPackage rec {
src = ./.;
buildInputs = [
rustfmt
pkg-config
cargo
rustc
libiconv
];
checkInputs = [ cargo rustc ];
doCheck = true;
CARGO_BUILD_INCREMENTAL = "false";
RUST_BACKTRACE = "full";
copyLibs = true;
# Optional things you might need:
#
# If you depend on `libclang`:
# LIBCLANG_PATH = "${llvmPackages.libclang}/lib";
#
# If you depend on protobuf:
# PROTOC = "${protobuf}/bin/protoc";
# PROTOC_INCLUDE = "${protobuf}/include";
name = cargoToml.package.name;
version = cargoToml.package.version;
meta = with lib; {
description = cargoToml.package.description;
homepage = cargoToml.package.homepage;
license = with licenses; [ mit ];
maintainers = with maintainers; [ ];
};
}
Then git add default.nix.
While you could technically moosh
flake.nix&default.nixtogether, I think it's much nicer to keep them separate.
Optional: nix-direnv & .envrc
In order to hook into nix-direnv:
Then git add .envrc.
Make sure it works
Before we do a full proper build, we need to populate the Cargo.lock! (Skip this if you have one!)
If you did not opt to use
nix-direnv, now you need to callnix develop .to enter the full development shell.
At this point you can try out cargo build and other relevant commands. They should work and use the libraries and tools provided by Nix.
Go ahead and test a build:
# Precautionary formatting...
Get trouble? Try running
nix build --print-build-logs --keep-failedto be able to see logs and check the workdir.
Here's it working:
In the real world
So how does this do on other crates? I test these files to several other popular Rust packages, here's what I needed to change for each:
ripgrep
I had to set mainProgram within the meta of default.nix:
# default.nix
{ /* ... */ }:
{
# ...
meta = with lib; {
# ...
mainProgram = "rg";
};
}
bottom
I also had to set mainProgram to btm. I also had to set doCheck = true in default.nix, as the tests depended on floating environment variables the build didn't have.
# default.nix
{ /* ... */ }:
{
# ...
doCheck = false;
meta = with lib; {
# ...
mainProgram = "btm";
};
}
Nushell (nu)
nu uses recursive git dependencies which are an issue for Naersk right now (#162), so I had to 'lift' those dependencies into the Cargo.toml as plain dependencies (with git and rev attributes):
# Cargo.toml
# ...
[]
= { = "https://github.com/apache/arrow-rs", = "9f56afb2d2347310184706f7d5e46af583557bea" }
In addition, I had to add openssl to the buildInputs in default.nix, and I set doCheck = false because some tests weren't able to touch certain places on the filesystem. Finally, like the others, I had to set mainProgram.
# default.nix
{ /* ... */, openssl }:
{
# ...
buildInputs = with pkgs; [
# ...
openssl
];
doCheck = false;
meta = with lib; {
# ...
mainProgram = "nu";
};
}
Getting help
For general help with Nix, consult the forums, for help with Rust try the forums.
I also find nix.dev, nixos.wiki, and the nixUnstable manual very helpful.
Need something more personal?
These days my consultancy is working with Indigenous organizations or persons. If you fall into one of those categories, please contact me at consulting@hoverbear.org.
If you're a tech startup, or anyone else, I would love to recommend you to use the services of Determinate Systems for Nix problems, and Ferrous Systems for Rust problems. They are owned and operated by people I trust.