What Is Nix
The major building blocks, at least in my mental model of Nix, are:
- The Nix Store
- Derivations
- Sandboxing
- The Nix Language.
The most basic, fundamental idea behind Nix is this:
Everything on your computer implicitly depends on a whole bunch of other things on your computer.
- All software exists in a graph of dependencies.
- Most of the time, this graph is implicit.
- Nix makes this graph explicit.
Once you’ve installed Nix, you’ll wind up with a directory at
/nix/store
, containing a whole bunch of entries that look something like this:
3mfcmgmpcqjajpdhfh8pdazmmd4vskns-nix-2.3.3-man/h9bvv0qpiygnqykn4bf7r3xrxmvqpsrd-nix-2.3.3/nrb3rkvwz114053yh00r7p2dlc9igp03-nix-2.3.3.drv
This directory,
/nix/store
, is a kind of Graph Database. Each entry (each file or directory directly under/nix/store
) is a Node in that Graph Database, and the relationships between them constitute Edges.
The only thing that’s allowed to write directories and files into
/nix/store
is Nix itself, and after Nix writes a Node into this Graph Database, it’s completely immutable forever after: Nix guarantees that the contents of a Node doesn’t change after it’s been created.
due to magic that we’ll discuss later, the contents of a given Node is guaranteed to be functionally identical to a Node with the same name in some other Graph, regardless of where they’re built.
What, then, is a “relationship between them?” Put another way, what is an Edge? Well, the first part of a Store path (the 32-character-long alphanumeric blob) is a cryptographic hash (of what, we’ll discuss later). If a file in some other Store path includes the literal text “
h9bvv0qpiygnqykn4bf7r3xrxmvqpsrd-nix-2.3.3
”, that constitutes a graph Edge pointing from the Node containing that text to the Node referred to by that path.
To demonstrate this linkage, if you run
otool -L
(orldd
on Linux) on thenix
binary, you’ll see a number of libraries referenced, and these look like:
/nix/store/gk9l41kp852lddrvjx9cfkgxwjs3vls8-libsodium-1.0.16/lib/libsodium.23.dylib
The transitive closure is an important concept in Nix, but you don’t really have to understand the graph theory: An Edge directed from a Node is logically a dependency: if a Node includes a reference to another Node, it depends on that Node.
Now here’s the key thing: This transitive closure of dependencies always exists, even outside of Nix: these things are always dependencies of your application, but normally, your computer is just trusted to have acceptable versions of acceptable libraries in acceptable places. Nix removes these assumptions and makes the whole graph explicit.
The second building block is the Derivation. Above, I offhandedly mentioned that only Nix can write things into the Nix Store, but how does it know what to write? Derivations are the key.
A Derivation is a special Node in the Nix store, which tells Nix how to build one or more other Nodes.
If you list your
/nix/store
, you’ll see a whole lot of items most likely, but some of them will end in.drv
:
/nix/store/ynzfmamryf6lrybjy1zqp1x190l5yiy5-demo.drv
This is a Derivation.
It’s a special format written and read by Nix, which gives build instructions for anything in the Nix store. Just about everything (except Derivations) in the Nix store is put there by building a Derivation.
The hash component of the Derivation’s path in the Nix Store is essentially a hash of the contents of the file.
Everything required to build this Derivation is explicitly listed in the file by path (you can see “bash” here, for example).
Since every direct dependency is mentioned in the contents, and the path is a hash of the contents, that means that if the dependencies and whatever other information the derivation contains don’t change, the hash won’t change, but if a different version of a dependency is used, the hash changes.
There are a few different ways to build Derivations. Let’s use
nix-build
outputs: What nodes can this build?
inputDrvs: Other Derivations that must be built before this one
inputSrcs: Things already in the store on which this build depends
platform: Is this for macOS? Linux?
builder: What program gets run to do the build?
args: Arguments to pass to that program
env: Environment variables to set for that program
A Derivation is a recipe to build some other path in the Nix Store.
what prevents builds from referring to things at undeclared paths, or things that aren’t in the Nix store at all?
Nix does a lot of work to make sure that builds can only see the Nodes in the Graph which their Derivation has declared, and also, that they don’t access things outside of the store.
A Derivation build simply cannot access anything not declared by the Derivation. This is enforced in a few ways:
- For the most part, Nix uses patched versions of compilers and linkers that don’t try to look in the default locations (
/usr/lib
, and so on).
- Nix typically builds Derivations in an actual sandbox that denies access to everything that the build isn’t supposed to access.
A Sandbox is created for a Derivation build that gives filesystem read access to—and only to—the paths explicitly mentioned in the Derivation.
What this amounts to is that artifacts in the Nix Store essentially can’t depend on anything outside of the Nix Store.
A Sandbox is created for a Derivation build that gives filesystem read access to—and only to—the paths explicitly mentioned in the Derivation.
What this amounts to is that artifacts in the Nix Store essentially can’t depend on anything outside of the Nix Store.
Nix has a custom language used to construct derivations. There’s a lot we could talk about here, but there are two major aspects of the language’s design to draw attention to. The Nix Language is:
- lazy-evaluated
- (almost) free of side-effects.
The Nix language lacks a lot of features you will expect in normal programming languages. It has
- No networking
- No user input
- No file writing
- No output (except limited debug/tracing support).
The Nix language lacks a lot of features you will expect in normal programming languages. It has
- No networking
- No user input
- No file writing
- No output (except limited debug/tracing support).
It doesn’t actually do anything at all in terms of interacting with the world…well, except for when you call the derivation
function.
The Nix Language has precisely one function with a side effect. When you call
derivation
with the right set of arguments, Nix writes out a new<hash>-<name>.drv
file into the Nix Store as a side effect of calling that function.
It’s worth emphasizing again: This is basically the only thing that the Nix Language can actually do. There’s a whole lot of pushing data and functions around in Nix code, but it all boils down to calls to
derivation
.
The Nix Language doesn’t ever actually build anything. It creates Derivations, and later, other Nix tools read those derivations and build the outputs. The Nix Language is just a Domain Specific Language for creating Derivations
Nixpkgs is the global default package repository for Nix, but it’s very unlike what you probably think of when you hear “package repository.”
Nixpkgs is a single Nix program. It makes use of the fact that the Nix Language is Lazy Evaluated, and includes many, many calls to
derivation
.