Photo - Ahmed Sobah
Photo

A Flake for your Crate

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

mkdir -p ~/.config/nix/
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
nix-env -iA nixpkgs.nixUnstable

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:

The first two are pretty self explanatory, let's dig into outputs! We only use a few of the available options:

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:

# 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.nix together, I think it's much nicer to keep them separate.

Optional: nix-direnv & .envrc

In order to hook into nix-direnv:

echo "use flake" > .envrc
direnv reload

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 call nix develop . to enter the full development shell.

cargo fetch
git add Cargo.lock

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.

cargo check
cargo build
cargo run --release

Go ahead and test a build:

# Precautionary formatting...
nixpkgs-fmt ./.

nix flake check
nix build .
nix run .

Get trouble? Try running nix build --print-build-logs --keep-failed to be able to see logs and check the workdir.

Here's it working:

$ nix run .
Hello, world!

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
# ...

[dependencies]
arrow = { git = "https://github.com/apache/arrow-rs", rev = "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.

            231666f96329f6c37d7736c5c0be5d5634043289