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:
name
- the name of the derivation,system
- the filesystem in which it can be built,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.