Introduction
What I love the most about winter are snow flakes! So you must imagine that when I stumbled upon nix and it’s flakes I was immediately very interested.
During my studies, I often had the chance to work on big projects which tend to span over multiple repositories, each with their own set of features needed to achieve a global goal. When using the project, cloning, installing and running the differents parts of the project can be a tedious task. To facilitate the process, we used Nix to manage the dependencies, configure and build the differents part of the project, and finally run it.
In this tutorial, we will see, step by step, how to use Nix and how to manage a simple project using Nix and why it can be convenient.
Requirement
Before we begin this tutorial, be sure to:
- Install the packet manager nix
- Enable nix’s experimental-feature: add this line in /etc/nix/nix.conf:
extra-experimental-features = nix-command flakes
. - Have some basic knowledge with Git and Linux ecosystem.
What is Nix?
Before we start let’s ask ourselves a question: What is Nix?
Nix can designate two different things: the Nix language or the Nix package manager.
The Nix Language
Nix is a pure functional language(just like Haskell or OCaml!), meaning that all functions are considered as any other value and do not have side-effect.
Additionally it is:
- Deterministic: An expression will always give the same results, given the same output.
- High-level: Most of the complexity is abstracted, and you don’t have to handle memory access.
- Lazy: Expression are only evaluated if necessary.
- Dynamically Typed: The type of an expression is determined at runtime.
- Declarative: The instructions are not carried out in the order in which they were written.
The language supports nine types of value:
- String: Can be used with
"
for single line strings or''
for multi-line ones:
''
Hello
This is a multi-line
string
''
- Integer:
42
. - Boolean:
true
orfalse
. - Null: The null value.
- Float:
42.42
. - Path: A path is a completely different type of data than string and are written
~/Downloads
for example. Unlike string, the paths are not surrounded by"
so be careful to not confuse those two types or you will spend a few minute understanding why things don’t want to go your way. - List: A list is an ordered collection of elements and can be composed of multiple value separated with a space. Like python list, a Nix list can contain multiple different types of value within the same list. We can thus have:
|
|
- Set: A set is composed of couples
key = value
separated with a;
(be careful the last couple also needs a;
!). As it is a set, there is no notion of order (and it won’t matter as the language is declarative and not sequential). Any type of value can be present as a value in the set, you can even put other fields from the same set! (you will need to add the keywordrec
before the set declaration). A typical set will have this form:
|
|
- Function: A function takes exactly one argument and return the processed value. In it’s simplest form you can write something like that:
|
|
Now you may ask, how do we make functions that takes multiple arguments? The answer is you can’t! You can, instead, make a function that takes one argument and returns another function that takes another argument and do that for each arguments. For example a simple add function would be:
|
|
In addition to the usual operators (boolean operators and arithmetic operator), Nix offer a few more interesting ones:
.
: Access the value of a set.
For example{hi = "hello"; theAnswer = 42;}.theAnswer
gives42
.?
: Test the existance of a value in a set.
This time{hi = "hello"; theAnswer = 42;}.theAnswer
givestrue
.//
: This operator merges two set together.
If we take{hi = "hello";} // {theAnswer = 42;}
we get{hi = "hello"; theAnswer = 42}
.++
: This operator performs the same thing as//
but for lists.
[1 2 3] ++ [4 5 6]
gives[1 2 3 4 5 6]
.
Finally, we will explore a few useful control structures:
-
Variable declaration:
In Nix you can only declare constant variables (pretty ironic I know).
For that you can use the structurelet vardecs in expression
.
As for the set each variable declaration is in the form ofname = value
and must be followed by a;
. All of the variable declared in thelet
part can be used in thein
part and the entire structure will take the value of the last instruction of thein
part. -
Conditional: Like many other languages, Nix allows you to conditionally execute code. For that you can use the syntax
if condition then branch1 else branch2
.
But contrary to other language (like C for example), a conditional structure must return a value and thus you cannot omit theelse
clause. -
Assertion: When coding it is very useful to detect error and stoping the program when encountering one.
In Nix we use the structureassert condition; expression
.
If the condition is false, the program will stop and it will raise an error.
If it’s true, the structure will take the value of the expression after the;
.
We can discover a lot more functionalities and fun things in the Nix language but we only need to cover the basics in this tutorial. If you want to find more about the possibilities of the language, I can only advise you to read the nix course of Litarvan where you will find more detailed informations.
The Nix package Manager
Nix can also designate the package manager, which double as a build system.
Nix is closely linked to nixpkgs, which is a github repository containing the recipes for almost every package you will ever need (except of course some very specific ones you need during GISTRE’s courses).
I say recipes because it does not contain the sources of the package you want to build but often a default.nix
file which will describe where to find the sources and how to build them with which dependencies.
The advantage is that when requesting the package from nixpkgs, the package will be built in an environment with only the right dependencies in their right version (which can be different from the one on your system).
Obviously, the process of building from source can be very long. To prevent that, nix offers a cache with prebuilt binaries for most of the package you can find in nixpkgs (except those who are under unfree license) stored at https://cache.nixos.org/. If you need, you can even create your own cache with your own binaries.
Nixpkgs additionally offer a large standard library of nix function you can use to better create your packages, operate on your sets or lists, manipulate your nix expressions and many other cool features. We will see some of them during this tutorial but if you want to learn more about nixpkgs here
Derivations
In the previous section I talked about recipes and packages.
Actually, in Nix there is no notion of “packages”, we call them derivations.
A derivation is not only a “nix-styled package”, it can also contain any types of files (generated files, configuration files, patch files to name a few).
Once they are built, derivations are stored in the nix store
which is a read-only directory where all of your derivations are stored using the format <hash>-name
.
As the directory is read-only if you want to modify something, you need to either rebuild the derivations if it’s yours or, if it’s not, you will need to recreate the derivation configuration locally and then rebuild it.
Now you may ask, why do we have to go through the trouble of all of that?
The answer is simple: reproducibility.
Derivations are build in isolated environments with only the dependencies we want inside.
This way, we can be sure that every time we build a derivation, we can be certain that we will have the same results.
We will now see how to build a short derivation. For that we will need a function from nixpkgs called mkDerivation
(nixpkgs.stdenv.mkDerivation).
This function is a wrapper around the nix builtin derivation
, and prefills for you some of the tedious fields need by derivation
.
Let’s study the following nix file where we create a derivation that exposes two simple executables.
|
|
As I just said, we use mkDerivation
to create our derivation.
This function takes a set as an argument which contains all the information required to build the derivation:
pname
: The name of the derivation.version
: The version of the derivation.buildInputs
: A list of other derivations this one depends on. Here, we includelolcat
to print a colorful hello message.src
: The sources of the derivations. Here, we use another function from the standard libraryfetchgit
which does exactly that: clone the sources from a git repository.fetchgit
takes another set with three keys:url
: The URL of the git repository. Here it points on the main branch of this repository.rev
: The sha of the commit (the weird alphanumerical string at the end of the URL when you select a commit on github/gitlab).hash
: Used to check the integrity and consistency of the fetched repository.
If you don’t know the hash value, you can, for now, just put a random valid hash likesha256-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxw=
.
buildPhase
: A string of shell commands to build our sources. Here we use make to create our executable.installPhase
: A string of shell commands to install our executable on our system. Here we install the generated executable inside the nix-store directory. For that we use the$out
variable, set by nix which contains the path to the created store. Finally we create a second executable, this time a shell script who will call our first executable and pipe it intololcat
. We use the variable${nixpkgs.lolcat}
to point exactly where the lolcat binary is as it will not be available in the PATH (except of course if you installed lolcat in your system before).
Once we have this file, all we have to do is build our new derivation!
For that we use the command nix-build
:
|
|
The first time your run this command you will certainly have the following error:
|
|
This will give you the right hash to use in the fetchgit.hash
.
Once you rebuilded your derivation, you should find yourself with a symlink to the nix-store named result
.
Inside there will be your two executables and if you execute hello_colored
you should see “Hello GISTRE!” colored using lolcat even if lolcat
is not installed on your system.
Nix flakes
Another very useful nix feature are the nix flakes.
They allow to manage projects by managing their inputs (dependencies) and outputs. As everything in Nix they allow good reproducibility and easy dependencies management but they offer some very cool additionals features.
If you type in your terminal:
|
|
You will find yourself with your very own flake!
|
|
Let’s review this flake step by step:
description
: A string describing what the flake is doing/used for.inputs
: A set containing all the dependencies of the flake.
Each of those dependencies is a git repository containing a flake and is represented by another set containing information on where to find the repository you need. You can also use repositories without flake as an input, but you will need to add the tagflake = false;
in the set of this specific input.
Here we only give the URL with the additionals information passed as query parameters but you can also fill the fieldstype
,owner
,repo
,rev
,ref
.
An equivalent to the first notation would be something like:
inputs = {
nixpkgs = {
type = "github";
owner = "nixos";
repo = "nixpkgs";
ref = "nixos-unstable";
};
};
outputs
: It is defined as a function that takes a set containing the flake itself (self
) and all the inputs (here we only havenixpkgs
) and returns a set containing everything our flake exposes. Here for example the flake exposes two derivations:default
andhello
. The first one takes thehello
derivation from the legacyPackages (here, legacy doesn’t mean deprecated, it’s just a naming convention) and stores it. The second one takes the first one and define it as the default output of the flake (if we call nix build without any arguments it will build this derivation). You can also note that the architecture and the os are specified. This is because most of the derivations are stored in a subset depending on which system you are using. Moreover the building strategy will be different for each system (Linux will use gcc and Darwin clang by default).
Once our flake is setuped we can use nix-build
to build our desired derivations.
We can also use the nix-run
command to directly build and run the executable in the derivation if there is one:
|
|
Another nice feature of flakes are devShells
.
They allow to create a development shell with specific derivations (either from nixpkgs or from your own flakes).
Those derivation will be made available inside this shell while not being accessible from you whole system.
This can be very useful when working on specific fields with technologies you don’t necessarily need to have on your whole os.
To declare a devShell you can add in the followings lines in the output section of your flake:
|
|
Now when running the command nix develop
, you will enter a shell where you can access all the derivation’s binaries easily.
Writting everything in your flake.nix
can make it difficult to read. To avoid that you can use the keyword import
to separate your code into subfiles.
You juste need to keep in mind that flakes and git are closely related as the flake will only “see” file that are tracked by git.
So if you ever have an error where a flake seems to not see a file, don’t forget to check if the file has been committed at least one before.
Also when working with a flake which depends on another of your repository, you will need to use the command nix flake update
to pull the modification you could have done on the others repository in your current one.
Finally, flakes works with a system of locking where each input version is stored in a flake.lock
to maintain the good reproducibility and easily see which version of an input you are currently using.
Building your first project
Now that we’ve seen all of that we can assemble everything we just saw. For that we will use the repository from before but on another branch where a nix flake is setup to build a library. You can find it here.
First, we will retake our flake from before and modify it’s inputs to add the repository as a dependency:
|
|
The helloNix repository will provide a libhello.a
and a hello.h
.
In our current repository, we will create a directory greeter
with a main.c
and a Makefile
:
|
|
|
|
Those two file are pretty standard and just build a binary with the library at LIB_PATH
.
Now that we have all of our files, we can build our derivation that will link the static library and our source code:
|
|
Here we create the binary by using our libhello
as a build input and then using the variable ${libhello.packages.${system}.libhello}
to get it’s path from the nix store.
Once this is done, we can create an devShell with our new derivation inside or add an apps
section to indicate that this binary destined to be executed.
|
|
The apps
sections allows us to directly call our binary with nix run
:
|
|
And of course you can still use nix develop
to access the binary directly without nix run
if needed.
At the end your flake should look something like this:
|
|
Conclusion
And that’s all you need to start manage your projects with Nix! It may seem like a bit of a learning curve at first, but once you get the hang of it, it really simplifies managing projects with multiple dependencies. For me, it’s been a game-changer, especially when working with projects spanning across different repositories. Plus, once everything is set up, you can easily share it with others and get consistent builds across different environments.
So, if you’re tired of dependency problems or just curious about a new way to manage your projects, give Nix a shot!