Nix in Braiins
Nix is a powerful package manager for Linux and other Unix systems that makes package management reliable and reproducible. It provides a declarative approach to package and configuration management and is designed to ensure that package installations are isolated from each other. This isolation prevents the common "dependency hell" and makes it easy to roll back changes. Nix stores all packages in unique directories in the Nix store, identified by hashes of their dependencies, ensuring that different configurations can coexist without interference. Its approach to package management is highly innovative and offers a high degree of flexibility, particularly for complex software environments.
Nix and Rust together work pretty well to create a powerful combination for reliable and efficient software development.
In Braiins, we use Nix in several places:
Keep in mind that you maybe do not have access to all of these repositories.
Why Nix for Rust?
Rust is known for its safety and performance, while Nix offers unmatched reproducibility and dependency management. The integration of Nix in Rust development can make some things a breeze
- Reproducibility: Nix ensures that your Rust projects are built in a consistent environment, avoiding the "it works on my machine" problem.
- Dependency Management: Nix handles complex dependency trees gracefully, ensuring that all dependencies are correctly versioned and isolated.
- Isolation: Using Nix, each project can have its own isolated environment with specific versions of the Rust compiler and dependencies.
Our main motivation were the following:
- Effective caching in CI - Nix hashes everything and keeps built derivations in a so called "nix store" typically located in the /nix/store folder of your local installation
- Dependencies for cross-compilation - In Tooling, we cross-compile to several different architectures for Linux and also produce builds of Toolbox for Windows and Mac
Example: Setting up Nix
To start using Rust with Nix, you first need to set up an environment.
Start by installing Nix, here's how to do it:
-
Install Nix: If you haven't already installed Nix, you can do so by running:
sh <(curl -L https://nixos.org/nix/install) --daemon -
Enable Flake Support: As of Nix 2.4, flakes are an experimental feature, so you need to enable them manually. Edit
/etc/nix/nix.conf(or~/.config/nix/nix.confif you're not using NixOS) and add:experimental-features = nix-command flakes
Here's an example of how to create a simple Nix shell environment for Rust development:
-
Create a file named
shell.nixwith the following content:{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell { buildInputs = [ pkgs.rustc pkgs.cargo ]; }This
shell.nixfile specifies that you want an environment withrustc(the Rust compiler) andcargo(Rust's package manager and build tool). -
Enter the Nix shell by running:
nix-shellThis command will download and install the specified versions of
rustcandcargointo a temporary environment. Note that while I show you how to do this with rustc and Cargo, there is a plenty of packages you can find here https://search.nixos.org/packages -
Once inside the Nix shell, you can start using Rust as usual. Any Rust project you build inside this shell will use the exact versions of Rust and Cargo specified in
shell.nix.
In our repositories, however, we use a more finetuned approach - managing Rust versions
with fenix and building Rust crates with
crane.
We will introduce these properly after we get into flakes.
Example: Adding Rustfmt and Clippy
Extend your shell.nix to include rustfmt and clippy:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [ pkgs.rustc pkgs.cargo pkgs.rustfmt pkgs.clippy ];
}
Now, when you enter the Nix shell, you'll also have access to rustfmt and clippy
alongside rustc and cargo.
Introduction to Flakes
Flakes are a feature in Nix that provides a more reproducible and manageable way of handling packages and configurations. A flake is essentially a function that has inputs and outputs, and locks dependencies to a specific version.
What are Flakes?
Flakes are a new way to manage Nix projects that bring several advantages over the
traditional nix-shell approach. They allow for:
- Reproducible Environments: Flakes lock the versions of all dependencies, ensuring that every user gets the exact same development environment.
- Declarative Configuration: Flakes make Nix configurations more readable and manageable by using a declarative syntax.
- Improved Dependency Management: Flakes handle dependencies in a more consistent and reliable manner.
Key Features of Flakes
The key features of flakes include:
- Lock File: Flakes generate a
flake.lockfile which pins the versions of all dependencies, making builds reproducible across different machines. - Inputs: Flakes can specify inputs (like Nix packages or other flakes) which are tracked and versioned.
- Outputs: Flakes produce outputs, such as Nix packages, NixOS modules, or anything else that can be built with Nix.
Flakes in the Context of Rust Development
In Rust development, flakes can be particularly useful for:
- Managing Rust Toolchain: Specifying the exact versions of
rustcandcargoensures that all developers and CI systems use the same compiler and tool versions. - Handling Dependencies: Rust projects often have numerous dependencies; flakes can manage these dependencies in a reproducible manner.
Example: Creating a Basic Flake
-
Start by creating a
flake.nixfile in your Rust project's root directory:{ description = "A simple Nix flake example"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; }; outputs = { self, nixpkgs }: { defaultPackage.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.hello; }; }This file defines a basic flake which re-exports the hello package from the nixpkgs repository. If you try
nix run .#, it should print "Hello world!". -
Build your project with:
nix buildThis command only builds the flake.
Here is an example with a custom derivation for the default package:
{
description = "A simple Nix flake with a custom derivation";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs }: {
defaultPackage.x86_64-linux = nixpkgs.stdenv.mkDerivation {
name = "my-custom-package";
buildCommand = ''
mkdir -p $out/bin
echo -e "#!/bin/sh\necho Hello from custom derivation!" > $out/bin/my-script.sh
chmod +x $out/bin/my-script.sh
'';
};
};
}
Using flakes in this way ensures that your Rust project is built in a consistent, reproducible environment, minimizing "works on my machine" issues.
Configuring the Rust Development Environment
We can see how to work with Rust by taking inspiration from the docker-spider project:
#![allow(unused)] fn main() { { inputs = { crane = { url = "github:ipetkov/crane"; inputs = { flake-utils.follows = "flake-utils"; nixpkgs.follows = "nixpkgs"; }; }; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; flake-utils.url = "github:numtide/flake-utils"; nixpkgs.url = "nixpkgs/nixos-unstable"; }; outputs = { self, crane, fenix, flake-utils, nixpkgs }: flake-utils.lib.eachDefaultSystem (system: { packages.default = let craneLib = crane.lib.${system}.overrideToolchain fenix.packages.${system}.minimal.toolchain; in craneLib.buildPackage { src = ./.; pname = "docker-spider"; }; }); } }
First, we provide toolchain via Fenix (this example is simple and just takes
the default minimal toolchain, but we can use it to get toolchain info from sources
such as the rust-toolchain.toml file as well).
Next, we create a simple crane package:
craneLib.buildPackage {
src = ./.;
pname = "docker-spider";
};
Entering the Development Shell: Use the nix develop command to enter a shell
with all the dependencies specified in your flake.nix:
nix develop
Inside this shell, you will have access to the Rust compiler (rustc), Cargo, and
any other dependencies you've added to your flake.nix.
Building and Running the Rust Project with Nix Flakes
To build and run your Rust project using Nix flakes, follow these steps:
-
Building with Nix: Use the
nix buildcommand to build your project according to the specifications in yourflake.nix:nix build -
Running the Built Executable: The output of your build will be in the
resultdirectory. You can run your executable directly from there:./result/bin/my-rust-project
That's about it for the introduction.