DEV Community

Gabriel Fontes
Gabriel Fontes

Posted on

How to package a Rust app using Nix

Also available at my website.

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
Enter fullscreen mode Exit fullscreen mode

Let's also generate a Cargo.lock:

nix run nixpkgs#cargo update
Enter fullscreen mode Exit fullscreen mode

And stage the files so that Nix sees them:

git add .
Enter fullscreen mode Exit fullscreen mode

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 ./.;
}
Enter fullscreen mode Exit fullscreen mode

Now let's build it:

# Nix3 command
nix build -f default.nix
# The nix legacy command also works
nix-build default.nix
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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 ./. { };
      });
    };
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You even get a dev shell for free:

nix develop
Enter fullscreen mode Exit fullscreen mode

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
  ];
}
Enter fullscreen mode Exit fullscreen mode

Nice. Let's try it:

# Nix3 command
nix develop -f shell.nix
# Nix legacy command
nix-shell shell.nix
Enter fullscreen mode Exit fullscreen mode

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 { };
      })
    };
}
Enter fullscreen mode Exit fullscreen mode

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

Calling it is easier and more reproducible now:

nix develop
Enter fullscreen mode Exit fullscreen mode

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 ./.;
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)