Using Nix for Package Management
As per their website, Nix is a tool that takes a unique approach to package management and system configuration. It creates reproducible, declarative and reliable systems. In this post I will cover how I have been using Nix to configure development and build environments.
Introducing Nix
Nix is a purely functional package manager. This means that it treats packages like values in purely functional programming languages such as Haskell — they are built by functions that don’t have side-effects, and they never change after they have been built.
Looking at their website we can see there are multiple parts to the project. The first is Nix which refers to the package manager as well as the functional expression language used to configure the packages and environments. The second is nixpkgs which is a collection of thousands of packages for the Nix package manager. Lastly, there is also NixOs which is an operating system based on the Nix package manager.
Admittedly, I still have a lot to learn about Nix. The project and its feature set is quite large. For the setup in this post we will be looking at some of the base features, but it is worth knowing that Nix also offers the following:
- Multiple package versions
- Multi-user support
- Atomic upgrades and rollbacks
- Language integration (Go, Python, Rust, etc…)
- Image building (Docker, Snap, etc…)
- Framework integration (Android, IOS, Qt, etc..)
Requirements
We are going to need a few things before we get started. The instructions below will be for Linux (WSL to be specific), but I will include links to the documentation in case you are running something else.
Nix
To install Nix we only need to run a single command:
|
|
Niv
Niv is a plugin for Nix that makes it easier to manage packages in a Nix project, track specific branches for packages, and also import packages from custom URLs and GitHub.
I have not mentioned this yet, but you can also use Nix to install system packages. We will be using it now to install Niv:
|
|
Assuming that all the steps during your Nix installation were successful, the Nix binary directory should already be added to your path and you can now use Niv.
direnv
You can alternatively use Nix to install direnv by running the following command:
|
|
direnv acts as an extension to your shell and allows you load/unload environment variables and virtual environments depending on your current directory.
The easiest way to install direnv is via Homebrew:
|
|
Once it is installed we also need to configure a hook for our shell. In my case I had to add the following to my ~/.zshrc file:
|
|
Initialising Our Project
We can now finally initialise our first development environment. First let us create a new directory for our project:
|
|
We will be using Niv to track a specific branch of nixpkgs. In our case we want to use the unstable branch to use the latest versions of the packages. Luckily, Niv includes a handy command to initialise the project and create your sources file:
|
|
You should see output similar to this:
Now that our sources file exists we can create a shell.nix file that makes use of it:
|
|
The code above will load our sources file, create a reference to nixpkgs, and lastly start a nix-shell. The final step is to ensure that our nix-shell is loaded when we enter the directory. The command below will configure direnv to do exactly that:
|
|
As soon as you run the above command you should see an error similar to this:
This is a security feature of direnv. To allow the script to run, execute the following command:
|
|
direnv should now initialise the nix-shell and you should see output similar to the following:
nix-create
To simplify all the above steps I have created a simple bash script:
Run the following commands to add nix-create as a global command:
|
|
Finding Packages
To find a package to install on Nix is fairly easy, thanks to their handy search page.
Nix is also capable of installing packages for specific languages. Search for a few of the following to see what I mean by this:
- nodePackages
- php81Packages
- go-swagger
CLI Search
As of the time of writing some of the Nix features are in beta and are hidden behind feature flags. One of these commands
is nix search
. To enable the command run the following:
|
|
You should now be able to search for packages via the CLI like this:
|
|
Adding Some Packages
Let’s start by adding Go version 1.17 to our project. To do this add the following line to the buildInputs in your shell.nix:
|
|
Your shell.nix should now look like this:
|
|
As soon as the file is saved the nix-shell should rebuild and the go
command should be available.
To confirm that you are using the version installed via Nix, run which go
and you should see
output similar to this:
Adding Custom Binaries
Sometimes you might need to add an additional custom binary to your environment. With the above setup this is easily done.
In our example, we will be adding the extremely useful tool jq via
the binary releases published on GitHub. The first step is to create a new file in ./packages/jq.nix
:
|
|
The above code creates a reference to the 1.6 release of jq for both 64bit Linux and OSX. Additionally,
we define an install step so that Nix knows how to handle the binary file downloaded. The last step is
to update our shell.nix
file to reference our new derivation.
|
|
We should now have the exact version of jq available wherever we run our nix-shell.
GitHub Actions
We can also use our nix-shell in our GitHub Actions. This will ensure a consistent environment during development, testing, building, and releasing. The workflow below will execute a single command while in the nix-shell context:
|
|
If you want to run a more complex script you can also use nix-shell as a script interpreter. Below is an example of an executable script that you can execute during your GitHub actions workflow:
|
|
Conclusion
With Nix you can easily define the exact packages you require for your environment. With the setup described above you can easily enforce which packages to use during the entire lifecycle of the application. This helps you achieve the following:
- Faster developer onboarding as the setup above will take care of all the dependencies
- Avoid situations where applications on CI/CD boxes are no longer compatible with your code
- No more “it works on my machine”
- Easier integration with custom binaries required in your environment
As I mentioned previously, this is a very simple introduction to what Nix can do. For a more in-depth guide check out Nix Pills published on the Nix website.