{ Gabriel Fontes }

How to package a Rust app using Nix

Here's a quick tutorial on how to nixify your cargo-based rust project

Date:

Tags: #nix #rust

Sometimes I see people looking around for a cookbook to package a rust project. Here’s a simple way to do it. As a plus, I’ll also show how to export the package through a flake.

I’ll use nixpkgs’ buildRustPackage. There’s a few other tools, my favorite being crate2nix, but we’ll leave that to a future tutorial.

Note: In this tutorial, we’ll be nixifying a crate on its own repo. If you want to package it elsewhere, remember to use fetchFromGitHub (and friends) with src instead of lib.cleanSource ./.;

Ready, set, go!

Let’s start by scaffolding a rust project with cargo:

nix run nixpkgs#cargo init foo-bar
cd foo-bar

Let’s also generate a Cargo.lock:

nix run nixpkgs#cargo update

And stage the files so that Nix sees them:

git add .

Okay, start by creating a default.nix file and add this minimal example:

{ pkgs ? import <nixpkgs> { } }:
pkgs.rustPlatform.buildRustPackage rec {
  pname = "foo-bar";
  version = "0.1";

  cargoLock.lockFile = ./Cargo.lock;

  src = pkgs.lib.cleanSource ./.;
}

Now let’s build it:

# Nix3 command
nix build -f default.nix
# The nix legacy command also works
nix-build default.nix

I swear. That’s really what you need for a working package. Nix’s happy path is really happy.

./result/bin/foo-bar
# Hello, world!

Flakefy it!

Let’s add a flake.nix now. Here’s a minimal example:

{
  description = "Foo Bar";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs }:
    let
      supportedSystems = [ "x86_64-linux" ];
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
      pkgsFor = nixpkgs.legacyPackages;
    in {
      packages = forAllSystems (system: {
        default = pkgsFor.${system}.callPackage ./. { };
      });
    };
}

Add any systems you want to support to supportedSystems list, of course.

The build is now more reproducible, as it will use the nixpkgs commit locked into flake.lock:

nix build

You even get a dev shell for free:

nix develop

Augment our dev shell

Let’s say you want additional tooling, such as a LSP, a formatter, a linter… You can augment this shell with additional packages!

Create a shell.nix file:

{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
  # Get dependencies from the main package
  inputsFrom = [ (pkgs.callPackage ./default.nix { }) ];
  # Additional tooling
  buildInputs = with pkgs; [
    rust-analyzer # LSP Server
    rustfmt       # Formatter
    clippy        # Linter
  ];
}

Nice. Let’s try it:

# Nix3 command
nix develop -f shell.nix
# Nix legacy command
nix-shell shell.nix

Note: If you use a shell other than bash and want to use nix develop with it, append a -c $SHELL to the command.

Awesome, we have augmented our shell with additional rust tooling :)

Let’s add it to our flake:

{
  description = "Foo Bar";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs }:
    let
      supportedSystems = [ "x86_64-linux" ];
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
      pkgsFor = nixpkgs.legacyPackages;
    in {
      packages = forAllSystems (system: {
        default = pkgsFor.${system}.callPackage ./default.nix { };
      });
      devShells = forAllSystems (system: {
        default = pkgsFor.${system}.callPackage ./shell.nix { };
      })
    };
}

I’ve replaced ./. with ./default.nix to make it more explicit.

Calling it is easier and more reproducible now:

nix develop

Grab metadata automagically

Setting the metadata (name, version) on two separate places (Cargo.toml and default.nix) is boring, we can do better! Let’s try out importTOML in our default.nix:

{ pkgs ? import <nixpkgs> { } }:
let manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
in
pkgs.rustPlatform.buildRustPackage rec {
  pname = manifest.name;
  version = manifest.version;

  cargoLock.lockFile = ./Cargo.lock;

  src = pkgs.lib.cleanSource ./.;
}

Nice.

Closing thoughts

AFAIK, this is the simplest way to package Rust crates. This should also make the project acessible for both flake/nix3 users and for those who still use nix-build and nix-shell.

I decided to keep this focused on buildRustPackage that, while simple and vanilla, has a few drawbacks (such as rebuilds starting from scratch). If you’re interested in incremental building, keep tuned: I will probably make a post about crate2nix soon(tm).

Spotted an error? See something I should improve or reword? Let me know at hi@m7.rs!