meain/blog

Nov 06, 2021 . 7 min

Introduction to nix

I have recently been playing around with nix a lot. It is a really interesting piece of technology. I haven't figured out most of it, but I have already started looking down on people who don't use it. But yeah, in all seriousness, it seems like a really interesting project.

Being a beginner I thought I would write a beginner ish tutorial on nix. Here is what nix is if you have not heard of it.

What is nix? #

Let us assume that you are a developer. I think I will go for a "Javascript" developer since that is what most people these days are anyways. And let us assume that you use npm for managing packages.

How would you go about installing a dependency?

npm install is-thirteen  # I have proundly contributed one commit to this mess

OK, do you know what that does to your repo? (No you don't, because you only think about yourself). Let me show you.

First thing that it does is modify your package.json file. is-thirteen gets added to the dependencies list.

diff --git a/package.json b/package.json
index 6ef6e28..d059850 100644
--- a/package.json
+++ b/package.json
@@ -8,5 +8,7 @@
},
"author": "Abin Simon <mail@meain.io>",
"license": "MIT",
- "dependencies": {}
+ "dependencies": {
+ "is-thirteen": "^2.0.0"
+ }
}

What this specifies is more or less "we need a version of is-thirteen which is above 2.0.0 and below 3.0.0". While that is useful, one thing that we all have learnt is that we cannot relay on people to bump versions properly. So we need to say what exactly is the version that we are depending on. That is where the package-lock.json file comes in.

diff --git a/package-lock.json b/package-lock.json
index e5ab36e..e293cdd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,21 @@
{
"name": "nixblog",
"version": "1.0.0",
- "lockfileVersion": 1
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "is-thirteen": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-thirteen/-/is-thirteen-2.0.0.tgz",
+ "integrity": "sha1-otvQ9at+EKTQGG6aCyQmMiTT+bE=",
+ "requires": {
+ "noop3": "^13.7.2"
+ }
+ },
+ "noop3": {
+ "version": "13.8.1",
+ "resolved": "https://registry.npmjs.org/noop3/-/noop3-13.8.1.tgz",
+ "integrity": "sha1-CuZBS21947bYUFXNKpIMGg1hrW4="
+ }
+ }
}

If you see above, we have a specific version of is-thirteen, ie 2.0.0. We also do this for any of the dependencies of is-thirteen. We pin the dependency noop3 to 13.8.1. This gets done all the way down the stack. What this means is that we can know exactly what version of everything we were using. But if you look into it, the dependency pinning stops at noop3. But what about the node version we are using? Shouldn't we pin that? What about the compiler used to build node? What about the kernel? What about any extra env variables/flags used while building the kernel?

That is exactly where nix comes in, it lets you pin everything all the way down. At least that is what the idea of nix is in general. There is a lot of advantages of doing this of which only one is that your setup is reproducible. To be frank, I got into nix just because I wanted to make sure I have a declarative definition of the packages that I install on my mac, but now I am too deep into this.

When talking about nix, you will primarily come across 3 different things, Nix, nixpkgs and NixOS.

Nix is the language in which all of the nix related stuff is written in. This is also the name of the package manager. nixpkgs is the primary repository for all packages. You can think of this as npm.org or debian/arch repositories. And finally NixOS is a complete operating system based on Linux which is built on the idea of nix. Your entire operating system in this case is declarative.

How do I use nix? #

Well, glad you asked. First of all, install nix. You can do this on a mac or linux machine.

https://nixos.org/guides/install-nix.html

This will install the nix package manager (not the NixOS operating system) on your system. Once you do that, you are ready to have fun with nix.

Let's first try working on a simple project using nix. How about we build a "Goodbye World!" program in Rust? You know it is fancy because we are using Rust ;)

OK, let's write the Rust program. You can drop this in main.rs.

fn main() {
println!("Goodbye World!");
}

Now, we have to compile this. And we do:

$ rustc main.rs
zsh: command not found: rustc

Ohh woopsie, we don't have rustc. Let's use nix to fix that. The simplest thing that you can do is to start a new shell with rustc in it. You can do that with:

$ nix-shell -p rustc

And now if we you run it, it all works. ;) Well, while that was cool, I could have done that with any other package manager.

I know, just wait. Give me some time to explain why nix is better. :D

What nix-shell does is that it puts you in a new shell which has rustc available. exit from the shell and see if rustc is still available?

No, right?

Let's get back into that nix shell with the same command and see where rustc comes from.

$ nix-shell -p rustc
$ which rustc
/nix/store/vqksgxrd1p091mnvz2bixnr8ylsyima1-rustc-1.52.1/bin/rustc

Not where you were expecting it to be from, I guess.

Let me tell you what is with that weird path. Everything that you install with nix is installed to /nix/store and just made available for you. So what happened when you did nix-shell -p rust is that rustc was installed to the nix store and made available via the $PATH variable for you.

Check what the $PATH variable contains inside the shell.

That was a lot of fun tricks, but let's make this thing a bit more declarative. Plus it would be a pain if I had to type out that command if I had multiple dependencies. That is wheres shell.nix comes in.

Drop this in a file called shell.nix in the root of your project.

with (import <nixpkgs> {});
mkShell {
buildInputs = [
rustc
];
}

Now, whenever you wanna work on this project, just navigate to the directory and do nix-shell. Nix will see this file and automatically load the env for you. If you are interested, there is a tool called direnv which will automatically "start up" this env for you as soon as you cd into that directory.

Let me explain what that code does thought. Not much, but here is how I that code converts to python:

import nixpkgs

make_shell(packages=[nixpkgs.rustc])

One thing to note here is that we are importing nixpkgs. What happens here is that you are importing whatever nixpkgs version you have in your local system, but if you wanted to pin everything, you can just specify an exact version there and you will be able to trace back to everything that resulted in building the rustc that you use.

Oh, I actually forgot to show you that this thing works now.

$ rustc main.rs
$ ./main
Goodbye World!

You can actually do a lot more than just specify the packages that you need there btw. You can specify the env variables, or some specific aliases in the new shell, or even start up some services or...

Checkout http://ghedam.at/15978/an-introduction-to-nix-shell to learn a bit more about nix shell.

Wait, but I want to build my own package with nix.

A wild nix file shows up. This one is called default.nix

default.nix is what you would use for packaging your own application.

{ nixpkgs ? import <nixpkgs> { } }:

nixpkgs.stdenv.mkDerivation {
name = "goodbye";
buildInputs = [ nixpkgs.rustc ];
src = ./.;

buildPhase = ''
rustc main.rs
''
;

installPhase = ''
mkdir -p $out
mv main $out/goodbye
''
;
}

Let me explain what the code does. Wait, let me convert it to python first.

class Goodbye(mkDerivation):
self.name = "goodbye"
self.src = "./." # current directory

def __init__(self, nixpkgs):
self.dependencies = [nixpkgs.rustc]

def build(self):
runShellCommand('''
rustc main.rs
'''
)

def install(self):
runShellCommand('''
mkdir -p $out
mv main $out/goodbye
'''
)

This is kinda what it translates to. The entire file in total is a function which takes nixpkgs (This part did not translate well into python) as an argument and creates a so called derivation. A derivation is essentially a set of instructions on how to build something, in this case our goodbye program.

If you check the code, we actually define a few things,

The whole deal with $out is a bit beyond this blog, but essentially we are specifying what all binaries are to be "installed".

Now that we have all of this defined, you can call nix-build in your project and you will get a result directory. This is where we take the derivation and build out the actual thing. If you check where the result directory points, you will again see that nix store path. As I said, everything goes into the nix store and all that you see is just symlinks. You can now run the app from the results directory.

$ nix-build
this derivation will be built:
/nix/store/c48jmny69bp3fyai34mbjirk3f9spxw8-env.drv
building '/nix/store/c48jmny69bp3fyai34mbjirk3f9spxw8-env.drv'...
unpacking sources
unpacking source archive /nix/store/5da7rd7iadk2dxj6w7nij615mvdx9sfv-rustgoodbye
source root is rustgoodbye
patching sources
configuring
no configure script, doing nothing
building
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/4mlqz3gx1n09gbcn95jap6rixpcvzdjd-env
shrinking /nix/store/4mlqz3gx1n09gbcn95jap6rixpcvzdjd-env/goodbye
strip is /nix/store/5ddb4j8z84p6sjphr0kh6cbq5jd12ncs-binutils-2.35.1/bin/strip
patching script interpreter paths in /nix/store/4mlqz3gx1n09gbcn95jap6rixpcvzdjd-env
checking for references to /build/ in /nix/store/4mlqz3gx1n09gbcn95jap6rixpcvzdjd-env...
/nix/store/4mlqz3gx1n09gbcn95jap6rixpcvzdjd-env

$ ls
default.nix main.rs result shell.nix

$ readlink result
/nix/store/4mlqz3gx1n09gbcn95jap6rixpcvzdjd-env

$ ./result/goodbye
Goodbye World!

Well, now you know how to build a package.

What else? #

Well, so far you know how to create a dev env for working on your project and how to build your own project using nix. This is the core of nix. The best part about the project that you build was that if you change the src to point to a git commit in a hosted repo instead of loading it from your local machine, then you have a fully declarative package under nix. This idea can be extended all the way down to the kernel and things used to build the kernel.

I have been playing around with NixOS and trying to move to it soon hopefully. I also have a personal server which I maintain fully using nix. I have declaratively mentioned the packages I want in there, building any at all if necessary. I can have my nginx config, systemd processes, and all the stuff that I need in that server declaratively mentioned. This is then version controlled using git. Now I have a version controlled state/history of my entire system.


This is pretty much the basics of how nix works in the context of building it for working on/building your projects. There is a lot more to nix and I am just scratching the surface here. But I hope that was a good introduction to nix.

Other resources:

Related projects:

← Home