11/2022

Developing with Nix

Nix is a tool to define and manage packages and dependencies in an explicit and reproducible way. It is also the name of the programming language used to create these definitions, and it is also a Linux distribution.

In this note we will have a look on how to use Nix to configure all our development setup.

developing-with-nix.jpg

Gentlemen, you are looking through a window... into another world.

-- Dr. Walter Bishop, Fringe

Toolchain

A toolchain is a collection of programming tools(software) we need in order to create any type of software, usually, these tools are installed globally in our machines and that just makes it very easy to "forget" about them.

We've all installed Node.js, we probably gave a try to Rust and Go, Terraform seems great for infrastructure and dhall looks pretty cool for configurations.

npm init {some-framework}
npx {some-npm-package}
brew install {tool}
curl {path-to-some-tool}.com | sh
{click-here to download and extract}

Package manager

Nix is yet another tool in our toolchain with a key feature of allowing us to define our tools and dependencies as nix packages.

with import <nixpkgs> { };
pkgs.mkShell {
buildInputs = with pkgs; [
pkgs.terraform
pkgs.go
pkgs.nodejs-16_x
pkgs.yarn
];
}

With the nix language we can create expressions to define all our tools and dependencies.

In the above example we are starting a nix-shell that contains the latest Terraform, Go, Yarn and the Node.js version 16.x (latest within version 16).

We can search for tools and their versions by running the command nix-env -qaP {toolName} or by searching for the tool in nix packages.

$ nix-env -qaP terraform
nixpkgs.terraform_0_12 terraform-0.12.31
nixpkgs.terraform_0_13 terraform-0.13.7
nixpkgs.terraform_0_14 terraform-0.14.11
nixpkgs.terraform_0_15 terraform-0.15.5
nixpkgs.terraform terraform-1.0.11
nixpkgs.terraform_1_0_0 terraform-1.0.11

Pure Shell

Nix-shell is an interactive shell based on a Nix expression containing all the specified packages, versions and dependencies.

We can start a nix-shell from a nix expression, given the file shell.nix with the following contents, we just need to run the command nix-shell shell.nix or just nix-shell (it will pick up the nix file expression).

with import <nixpkgs> { };
pkgs.mkShell {
buildInputs = with pkgs; [
pkgs.deno
];
}

This will start a nix-shell with the latest version of deno, but it will also contain all the global packages and tools installed in our machine, if our application requires terraform and we have installed it globally we will not know about it until we try it in a different machine.

With the command nix-shell --pure or nix-shell shell.nix --pure we can guarantee that only the dependencies and versions defined in our buildInputs are available, so in the above case we will only have the deno cli available in the nix-shell, nothing more.

We can also start an interactive nix-shell without the need of creating a nix expression, we can just write the command nix-shell -p deno --pure and we will have a pure shell with the latest version of deno.

Scripting

Most applications need scripts to help on some day-to-day tasks, sometimes we just create a myScript.sh file to help with those tasks, other times when we are using Node.js we just abuse the utility of scripts in package.json.

{
"name": "very simple tool",
"scripts": {
"postinstall": "doSomething",
"cleanup": "cd somewhere && rm -rf something",
"before": "yarn clean && ./myScript.sh",
"build": "yarn before && buildSomething"
}
}

With Nix we can just create a writeScriptBin or writeScriptBin and make sure our scripts part of our shell and our packages. We can also use any programming language to write the scripts by defining the environment.

with import <nixpkgs> {};
let
update = pkgs.writeShellScriptBin "update" ''
#!/usr/bin/env node
console.log('we run node here')
'';
build = pkgs.writeScriptBin "build" ''
${pkgs.yarn}/bin/yarn
${pkgs.yarn}/bin/yarn build
'';
in
pkgs.mkShell {
buildInputs = with pkgs; [
pkgs.terraform
pkgs.nodejs-16_x
pkgs.yarn
update
build
];
}

In this example we have a build and update script that use tools from the list of packages and are available in our shell. We can just run the command nix-shell --pure --run "build" and we will see the console.log.

Pinning Nixpkgs

So far, we are using Nix expressions with with import <nixpkgs> {}; as a starting point of our packages, although this is an easy and fast way to showcase a Nix expression it is not the best way to make sure our expression is fully reproducible and it will make our application conditioned to the nix channel version that was installed in our machine.

To make sure our Nix expression is fully reproducible independent of when or where we run our application we should pin our Nix expression to a specific Nix channel:

let
pkgs = import (fetchTarball {
name = "nixpkgs-22.05-darwin";
url = "https://github.com/NixOS/nixpkgs/archive/2b37d567bb7e.tar.gz";
sha256 = "1bmy61s15grwa86i6s8q47zqxvnbchjyv0gby2484rsa35v1dw6p";
}) { };
in pkgs.mkShell {
buildInputs = [
pkgs.nixfmt
pkgs.deno
pkgs.nodejs
];
}

In order to obtain the available channels we can visit Nix Channel Status and select the nixpkgs-... stable or unstable version, with the commit ID we can now pin our packages and retrieve the sha256 value by running the command:

nix-prefetch-url --unpack https://github.com/NixOS/nixpkgs/archive/{commit-id}.tar.gz

We can also find older Nix channel status for more legacy applications in the channels repository.

Building

Start a interactive shell with all the required dependencies seems pretty easy, but if we want to have a fully reproducible we will have to make sure Nix knows about our application packages, and that can differ if we're using npm or go or python.

In this case the implementation will depend on the language support.

We can always use a nix-shell to build our application output, it will just not be fully reproducible.

Cleaning

Often we install tools just because we would like to try them out in a project, and it turns out we will not use it again.

The command nix-collect-garbage will take care of that as it will make sure all our old non-used packages will be removed from our machine.

You're still here?

Thank you so much for reading this!

If you are curious on how I use nix for my personal projects just go ahead and check out the repository for my website, there's plenty examples of Nix.

Until then!