NIX: 2 Simple derivations

NIX

Experience

I’ve been using NixOS and I quite like it (a lot). When I installed it, I got rid of my Debian and jumped head forwards with a full wipe out and clean instal. To be honest, it wasn’t that bad, and I managed to install everything in two attempts with around 1 hour of tweaking in the liveUSB version - but the documentation was nice, clear and easy to follow.

To begin with, it took a while to get used to it, but worked it in and in the end it turned out I really like it - and it works very well. So I thought that it might be worth sharing a few steps and insights into making a simple standard derivation.

Why NixOS

If you like declarative environments, changing your configurations, trying out new software, reproducible builds, etc. then NixOS is for you. You can work, configure, build, run programs in completely different environments and it is as easy as writing a nix derivation.

A tale of two derivations

What are we looking to do?

It’s important that we stick to the basics - let’s make two simple nix derivations. The first one will produce a file with that says “Hello World!” and the second derivation will use the first one and produce another file that says a different message.

From \null To NIX - A bit about the NIX language

NIX Functions

NIX the package manager uses its own lazy, pure, functional language to build derivations called NIX - and as you would expect functions play a major role in both NIX the language and NIX the package manager.

*I recommend you try these out in a nix repl instance. So let’s have a look:

f = x : x * 2

f 2 # -> 4

As you can see, all the functions are actual annonymous functions that can be bound to a variable. To have multiple arguments to a function we need to curry it:

f = x : y : x * y

f 2 3 # -> 6

Functions that take sets

A handy feature of NIX is the way it can manipulate and work with sets. For example:

We right sets like this:

{ a = 1;
  b = 2;
  c = "Text"; }

Note the use of ; to distinguish between elements of the set. So a function that works on a set would look like this:

f = s : s.a * s.b

f { a=2 ; b=3; } # -> 6

And with a named set:

b = { a = 3; b = 4; c = 5; }

f b # -> 12

Note that you can even explicitly grab these arguments from the set on the left hand side of the expression like so: (careful with the fact that when you destructure it on the right hand side it uses , always gets me)

f = {a, b} : a * b

Furthermore, NIX allows you to pass default arguments to your set defined functions: (also note that even if the set has only one element you still need to put the ; - also gets me every time).

f = { a ? 1 , b ? 2 } : a * b

f {}         # -> 2
f { a = 2; } # -> 4
Set Union

By the way, you can merge sets using the infix operator //. As the union of two overlapping elements the right hand side is maintained.

{a = 1;} // {b = 2;} # -> { a = 1 ; b = 2; }
{a = 1;} // {a = 2;} # -> { a = 2 ; }
List Concatenation

List concatenation is your usual suspect. ++

[ 1 2 ] ++ [ 3 4 ] 
Unexpected attributes

A thing to note is that there is a difference in declaring the variables of the set on the left or just declaring the set and destructuring on the right. The differe is that when it is on the left the function will not like it if you send it a set different to what it expects.

f = { a ? 1 , b ? 2 } : a * b
g = s : s.a * s.b

f { a = 1; b = 2; c = 3;} # -> will not work! because it has a c 
g { a = 1; b = 2; c = 3;} # -> will work

To allow for more flexible sets in your function you need to add ... to its expected arguments

f = { a ? 1 , b ? 2, ... } : a * b

f { a = 1; b = 2; c = 3;} # -> 2 all good

Also note that this way of writing it has the advantage of being able to give the arguments default values. So all in all, pick your weapon wisely.

Note: If you are like me and like static typing check out dhall is. I haven’t had the chance to use it much, but plan on doing so soon.

Lazy is good

Bein Lazy, means that every NIX file is essentially a set of expressions! So you can use import to import one file into another, and NIX will essentially import the other file as a set - that’s awesome. So for example:

File a.nix:

2

File b.nix:

{ b = 3; }

File f.nix:

s1 : s2 : s1 * s2.b

We can the open back up our nix repl and do the following:

a = import ./a.nix
b = import ./b.nix
f = import ./f.nix
f a b   # -> 6

That’s great if you ask me.

With Let In

Let and in allows us to intorduce terms on the go, while with allows us to avoid writing a lot.

The use of with makes a lot of sense when you realise that nested expressions always have the dot notation - and it can become tiresome. With gets rid of that by making all of the variables (while avoiding clashses) of a term available to the other term. For example:

Instead of:

let                         
  context = { a = 1; b = 2; };
in
  context.a + context.b

We can write:

let                         
  context = { a = 1; b = 2; };
in
  with context; a + b

Inherit

Inherit allows you to bring into the local scope, variables from outside the scope. It sounds more complicated than it is but here’s an example:

a = 2
b = 3
c = { inherit a b; c = 4; }

c # -> { a = 2; b = 3; c = 4; } swoosh!

Rec

Rec turns a set into a set where self referencing is possible. It’s handy as you can write out a sequence of lets and just grab one of them:

rec {
  a = 1032 - b;
  b = 3213 - c;
  c = 222;
  }.a # -> -1959 which is the value of a

Recursivity

To make use of recursivity you need to declare your function with a let

let f = x : y : if (y <= 0) then x else f ( x + 1 ) ( y - 1 );
in  f 3 4 # -> 7

Awesome.

NIX Types

Although not statically typed, NIX is strongly typed. So the basic types in NIX are:

Type Example Details
Strings ''Foooo'' '' or "
  "Bar"  
Integer 4  
Floats 2.2  
Path ./ Relative paths get to be
  ./. expanded to absolute paths
  <nxpkgs/lib> <nxpkgs> - goes to your
    NIX_PATH
URI https://cstml.github.io/  
Boolean true,false  
Null null  
Lists [1 2 3 “Bro mixing type” ] Spaces, not commas
Set { key = value; } Don’t forget the ;
Function x : x + 1 Awesome with curry

Nix built ins

Nix has LOADS of builtin functions, which I’m not going to go through, but which you can easily discover by going into the nix repl, typing builtins. and hitting Tab. If you are a HaskellHead or any sort of functional fan you will see a lot of the usual suspects in there.

To learn what a function does, I sometimes do this thing where I type the function and evaluate it without an argument and then it will give some indication of what it is expecting. Somehow, I still haven’t found a great condensed easy to search resource for all the nix functions, but this works great - REPLS are fantastic.

Example

builtins.derivation 1  # error: value is an integer 
                       # while a set was expected, at (string):1:1 - oh! Okay
                       
builtins.derivation {} # error: required attribute 'name' missing, - ah, I see

# ... - you catch my drift 

But even cooler nix gives us builtins.derive.attrNames which gives us all the expceted attributes of a Set:

builtins.attrNames (builtins.derivation {})
# ->  [ "all" "drvAttrs" "drvPath" "out" "outPath" "outputName" "type" ]

Further reading


The first derivation

Ok, so it is very important to get to grips with the language (to a good level) first I would say! Because afterwards you will se how nice and neat working with NIX is.

What is a derivation

A derivation is a NIX pkg manager defined set (if you would like). Every NIX derivation set must contain the following keys:

  1. name - the name of the derivation,
  2. system - the filesystem in which it can be built,
  3. builder - the binary program that builds the derivation

Upon receiving a derivation, nix knows what to do with it. In essence it evaluates it and does some funky stuff with hashes, so that way you always get to save stuff in the right place. (I quite like cryptography and hashes and am unsure what the collision resistance of the hashing algorithm really is, but I haven’t heard of any collisions yet)

Let’s create our greeter derivation

Derivation 1

greeter.nix:

{
  pkgs    ? import <nixpkgs>{},
  message ? "Hello World!",
  sayHi   ? '' echo '\n ${message} \n' \ >> $out '', 
  ...
} : builtins.derivation({
  name    = "greeter";
  system  = builtins.currentSystem;
  builder = "${pkgs.bash}/bin/bash";
  args    = [ "-c" sayHi ];
})

Ok so what is the derivation actually? Well it is as we have seen from the tutorial above a function that takes in a set and returns the result of a derivation. In this case our function takes as parameters message and sayHi. As previously discussed we use the builitns functions to get the our current system and the builder is the nixpkgs.bash - it doesn’t get any more basic than this really. To bash we send first the argument -c which means execute the next arguments as commands, and then we send it an the sayHi script from the argument Set. If you look at the function you can see that we are outputing to an environment variable specifically $out. This is where NIX the package manager will save and store the output of our build. After running nix-build greeter.nix you should have a result symlink in your current folder that if you run cat on you will see our message. Great!

Derivation 2

As we said from the beginning, we want derivation two to build upon greeter and essentially do the same thing but with a different message. This is where NIX’s elegance comes into play.

greeter2.nix

let
  g = import ./greeter.nix {};
in
{ pkgs    ? import <nixpkgs>{},
  greeter ? import ./greeter.nix{
    message = "New Message";
  },
}:builtins.derivation({
  name    = "greeter2";
  builder = greeter.builder;
  system  = greeter.system;
  args    = greeter.args;
})

Ok, so in greeter2 we are essentially reimporting greeter1 and inheriting everything from it while just changing the message. But you might say this is not necessarily the point! We already have a function, that does this, why do we need to build another one?! Can’t we just change the argument we are sending to the function in the first place? And the answer to that is yes! - great spot

Derivation 3

greeter3.nix

{
  g = import ./greeter.nix { message = "Something else"; };
}

And that’s it! We’ve parametrised the build.

Conclusion

I realise this is a long post, and i might break it down. But hopefully it has been helpful and it gives you a place to start in the world of NIX. I highly recommend it.

Further reading