Contents
last updated 2024-01-29 for nix 2.13 - 2.18, nixos 23.05 - 23.11
Introduction to Nix
Hello! This is my guide to understanding the build automation software/package manager called Nix, and the Linux distribution built on top of it called NixOS. I hope it helps :)
If you have any questions, complaints, or suggestions, contact us!
Installation
You'll need to have Nix on your system to follow along with some of the demonstrations. You can still read this document without a Nix install, but you might find it helpful to observe its behavior yourself.
We won't be covering all of the features of every command we use, so if
at any point you want more information, you can pass --help
to
any command or subcommand you want to use, or consult
the Nix manual.
If you don't already have Nix installed, you can download it here. The option of a "multi-user" or "single-user" install doesn't matter for the purposes of this guide, so feel free to follow the directions there and decide what works for your system.
At the time of writing, Nix's new commandline interface introduced in Nix 2.0 is still marked experimental. Since its design is more intuitive than its predecessor and will eventually replace it, we're going to be using it anyway. Be aware that some details may have changed since the version this guide was written for.
To enable the features we need, you should modify the Nix config file at
~/.config/nix/nix.conf
, creating it if it does not exist, and add
the following line:
experimental-features = nix-command flakes
What is Nix?
1. A content store
The Nix store (default /nix/store/
) is a
repository of files and directories ("objects") each
identified by a unique name and hash. The simplest way to interact with a
store is to add a fixed object, hashed by its content.
$ echo "Hello, world!" >hello
$ nix store add-path hello
/nix/store/rw26ab68fybv6fyycyz61k7rvdgqgv06-hello
Any Nix store, given this file with this name and content, will place it at the same store path you see there. As a result, all Nix stores that you trust constitute a big distributed cache you can freely copy objects between!
$ HELLO=/nix/store/rw26ab68fybv6fyycyz61k7rvdgqgv06-hello
# give the system at example.org my hello file
$ nix copy --to ssh://example.org $HELLO
# and now i can delete it locally
$ nix store delete $HELLO
1 store paths deleted, 0.00 MiB freed
# because i can get it back whenever i want :)
$ nix copy --from ssh://example.org $HELLO
2. A reproducible build system
The property that store paths uniquely identify their objects has further implications once you introduce the derivation, a special store object that represents a build recipe. That is, a function that consumes existing objects as input and produces new objects as output.
Derivations are forced to identify all their inputs as store objects, so they will always be given the exact specified versions of the particular resources they need, and nothing more. We can assume a derivation with a particular store path has enough information to reproduce the same build every time on any system of the same platform.
Given this assumption, we can also decide that hashes for output objects are based on the derivation that created them, and thus refer to a derivation's output paths before we actually build them.* This results in two important properties:
- Derivations can lazily depend on the store paths of other derivation outputs as input. The dependency only needs to be built when the dependent that refers to it is built.
- If you know the store path for an output before building it, you can just ask another store if it has a copy, and Nix can transparently substitute an expensive build for a cheap copy from another store. Source-based packages, but with binary caching for free!
In general, Nix is aware of object dependency relations, not just for derivations' dependencies but for outputs' too. Nix will copy not just the output object but the set of every object that its contents refer to (its closure, in Nix terminology).
Naturally, if you try to nix store delete
any object that has
dependents (or, "referrers"), Nix will refuse to do so.
One thing I want to emphasize here is that store objects have no notion of a
"system package". Conflicting dependencies are unrepresentable, because each
program specifies exactly which object they want. Nix doesn't care
that your store might have both 123...-libfoo
and
abc...-libfoo
, dependents will specify the full path anyway.
The same goes for user programs, of course: there's nothing wrong with one
user's imp...-python3-3.12.1
existing on the same system as
another user's 3iy...-python3-3.13.0a2
, as neither will be forced to
take the role of the global /usr/bin/python
.
We'll talk about producing our own derivations in a supplementary document after this one. For now, let's get into the main purpose for Nix as an end user.
3. A source-based package manager
Nix comes with several built-in conveniences and abstrations to make it more suitable for conventional package management. To start with, Nix knows about remote package repositories, and how to build derivations from them:
nix build nixpkgs#firefox
The argument nixpkgs#firefox
is a reference to
something that can be built, called an installable, which
we'll discuss in detail soon. Abstractly, it's pretty simple: we're asking for the
derivation called firefox
that lives inside the Nix project
(or, "flake") called nixpkgs
.
You can search Nixpkgs for more packages on
search.nixos.org, or you can
use nix search nixpkgs <query>
(but that command needs to
evaluate all of Nixpkgs locally, so it can be slow)
When you run that command, it probably won't actually build anything! As previously discussed, Nix can substitute builds with copied outputs from other stores, and Nix is configured with
substituters = https://cache.nixos.org
by default. This allows every unmodified package in Nixpkgs to be substituted automatically.
Build commands are also idempotent, so it's safe to run them just to "make sure" an output exists in the store. If it's already there, nothing will happen.
So, regardless of whether your system actually needs to build the specified
derivation or not, now we know that there's a path in your Nix store that has
Firefox in it. By default, nix build
will additionally plop a
symlink called result
in your current directory, which leads to
the relevant store path.
Nix has the capability to delete outputs that are no longer being
used in order to save space (nix store gc
), and many systems
are configured to do this automatically on a timer.
The garbage collector will never delete:
- direct GC roots (store paths symlinked in
/nix/var/nix/gcroots
or/nix/var/nix/profiles
) - runtime roots (store paths in working directories, file descriptors, or environments of running processes)
- any store object that refers to any of the above at runtime, recursively
So, an additional symlink to your result
is placed in
/nix/var/nix/gcroots/auto
so that the garbage collector can't
pull the thing you just built out from under you until you delete
result
. That's why this particular build command chooses to
communicate its output directory with a symlink.
Nix also comes with some other build commands that make it more convenient to use with runnable programs.
nix run
finds the main executable inside the derivation output
and runs it:
$ nix run nixpkgs#git -- --version
git version 2.42.0
nix shell
opens up a subshell with a PATH that includes all the
bin
directories from the derivation(s) you specify:
$ nix shell nixpkgs#cowsay nixpkgs#fortune
$ fortune | cowsay
_______________________
< You do not have mail. >
-----------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
$ exit
$ fortune | cowsay
bash: cowsay: No such file or directory
bash: fortune: No such file or directory
or, if you really want the package in your user's PATH persistently, like a more traditional package manager, you can use Nix's profile system.
Profiles
Profiles are a way to merge several derivations' outputs one-by-one into a single store path. This offers a more familiar package management interface:
# the same build of nixpkgs#firefox will remain in your profile permanently after install
$ nix profile install nixpkgs#firefox
# until you decide to replace it with a new version
$ nix profile upgrade nixpkgs#firefox
# or remove it entirely
$ nix profile remove nixpkgs#firefox
By default, your user's profile is linked at ~/.nix-profile
,
and its bin
subdirectory is placed in your PATH. This makes it
easy to persistently install software you want to be available all the time,
like with a traditional package manager.
--profile /nix/var/nix/profiles/per-user/<username>/profile
)
to reference other users' profiles, or profiles created for other purposes. Since store objects can't be modified directly, a profile is actually a hive of
symlinks, each to different "generations" of its output. You
can see what you changed each time with profile history
, and you
can profile rollback --to <version>
to revert to a specific
generation at any time.
Using profiles as a versioning mechanism carries over to other Nix-based projects, too, like NixOS systems!
nix profile rollback --to XXX --profile /nix/var/nix/profiles/system
The idea is that sometimes destructively modifying the state of your environment is necessary or convenient, but these modifications should be atomic, reversible transactions so that you can always get back to a known-good state.
Flakes and Installables
As mentioned before, that nixpkgs#foo
string you give to Nix
is an example of an installable, a reference to something
that can be built by Nix. Specifically, it's a reference to some attribute
foo
inside the flake called nixpkgs
.
A flake is like a "project file" for Nix. It's a way to represent a collection of Nix code that provides certain well-defined outputs, like a package repository, or a Nix library, or a set of NixOS configurations. The Nixpkgs repo, for example, uses its flake to expose the derivations for its software packages so you can reference them by name on the commandline, as we've already seen.
Flake outputs can depend on a set of inputs in the form of other, independently-controlled flakes. For the sake of reproducibility, exact revisions of these inputs are saved in a lock file, with a hash of their contents. That way, a given revision of a flake consistently uses the intended revisions for all its input flakes.
The flake reference nixpkgs
is not just a hardcoded value. It's
really just an alias in a flake registry, which we can look
at with nix registry list
. There you'll find a
global mapping to the rolling-release Nixpkgs branch on
GitHub:
global flake:nixpkgs github:NixOS/nixpkgs/nixpkgs-unstable
This means that wherever you typed nixpkgs
, you could type
github:NixOS/nixpkgs/nixpkgs-unstable
instead.
If you wanted to use, say, the Nixpkgs revision for a particular NixOS
release, you can specify a different branch while still using the registry
alias for convenience, like nixpkgs/nixos-21.11
. This
works for other registry aliases with optional parameters, too.
The default global flake registry is downloaded from a file managed by the NixOS project. However, you can override its values and add new ones with alternative mappings:
nix registry add nixpkgs ~/my-nixpkgs-fork
nix registry pin nixpkgs
(pins nixpkgs to the exact revision it's at)nix registry remove nixpkgs
Without specifying a file with --registry
, these commands will,
by default, add or remove mappings in the user registry located at
~/.config/nix/registry.json
. There's also a lower priority
system registry at /etc/nix/registry.json
,
which is intended to be auto-generated from the system configuration on NixOS.
Any directory (but usually a Git repository) containing a special Nix file
called flake.nix
is recognised as a flake. As we've seen, flakes
can be referenced using a name in the registry, a local file path, or through
special URL-like syntaxes like
github:<user>/<repo>[/branch]
. You can find a full list
of these reference syntaxes
in the manual.
In addition to specifying an installable from a flake reference, you can also
use an already existing path in the store (/nix/store/...
), or an attribute in a non-flake Nix
file with
--file /path/to/file.nix <attr>
,or the result of a Nix expression with
--expr '...'
.The Nix Language
Derivations, flakes, and various other bits of Nix-related configuration are expressed in a pure functional DSL called the Nix language. An overview of the language's syntax follows.
You can follow along in nix repl
, if you'd like! In the repl,
you can define variables imperatively like x = 1234
for
convenience, but be aware that this doesn't exist in Nix code proper: every
Nix file is a single expression; no statements.
Types
## strings
"hello, world!"
# with interpolation
"the result is ${some-string}"
# if you want to escape a literal "${", you can prefix it with two single quotes
"''${this is not interpolated}"
# two single quotes also mark a multiline string
''
line 1
line 2
''
# equivalent to "line 1\nline 2\n"
# (note that a single leading newline is ignored, but a trailing one is not)
## integers and floats
1
3.14
## booleans and null
true
false
null
## lists
# space-seperated values!
[ 1 2.0 "three" ]
## attribute sets (associative arrays)
{
a = 1;
b = 2.0;
c = "three";
}
# shorthand equivalent to { a = { b = { c = 1; }; }; }
{ a.b.c = 1; }
{
"blah..." = 1; # attribute names can be quoted if they have special characters
${foo} = 2; # and they can be interpolated from other values
}
# `rec` allows attributes to be defined in terms of other attributes in the same set
# (it's short for 'recursive')
rec {
a = 4;
b = a * 2;
}
## filesystem paths
/absolute/path/to/file
./relative/${path}/to/file # interpolation works in paths, too
./. # this refers to the current directory; `.` and `./` are not valid path syntax
# if a path is in angle brackets, the starting directory is found in the
# environment variable `NIX_PATH`, a space-seperated list.
#
# since this functionality is "impure", it isn't accessible in all contexts,
# and you may need to add `--impure` if you ever use it in a Nix command.
# if `NIX_PATH="foo=/path/to/dir"`, then
<foo/bar>
# becomes
/path/to/dir/bar
# if `NIX_PATH=/path/to/dir`, all of the subdirectories in `/path/to/dir`
# are considered.
#
# so, if `/path/to/dir` contains the subdirectory `foo`, then
<foo/bar>
# becomes
/path/to/dir/foo/bar
# if not otherwise defined, nix behaves as if
# `NIX_PATH="~/.nix-defexpr/channels /nix/var/nix/profiles/per-user/root/channels"`.
#
# "channels" are a concept from pre-2.0 made redundant by flakes. all you need to know
# is that some Nix code in the wild will rely on this behavior to refer to a checkout
# of the nixpkgs repo:
<nixpkgs>
## functions
# this is a function that takes an argument `n` and returns `n + 1`
n: n + 1
# functions are called with simple juxtaposition, like `f x`
(n: n + 1) 2 # = 3
# they only take one argument, but currying works
(a: b: a + b) 2 4 # = 6
# or, you can accept a set argument and use a pattern matching syntax to destructure it
({ a, b }: a + b) { a = 2; b = 3; } # = 5
# when you're destructuring a set argument, you can also use @ to bind a name
# to the set as a whole
set@{
normal,
# and ? to specify defaults for optional attributes
optional ? 2,
# and ... to allow additional, unspecified attributes to be present in the set
...
}: normal + optional
## derivations
# derivations are a subtype of attribute set ({ type = "derivation"; /*...*/ }), but
# they're special-cased by the Nix machinery enough to sort-of call them their own type.
# there's too much to talk about here; they'll have their own section linked at the end!
derivation { /*...*/ }
Operators
In order of precedence!
## accessing an attribute in a set
set.attr
# fallible access!
set.attr or "default"
## applying a function
f x
## negating a number
-n
## checking for the existence of an attribute
# returns `true` if `set` contains an attribute `x`
set ? x
## concatenating lists
listA ++ listB
## doing arithmetic
a * b
a / b
a + b
a - b
## concatenating strings and paths
stringOrPathA + stringOrPathB
## negating booleans
!p
## merging two sets into one
setA // setB
# if both sets have an attribute with the same name, the one from the second set is used
{ a = 1; b = 2; } // { b = 3; c = 4; } # = { a = 1; b = 3; c = 4; }
# the merge is shallow with regard to nested sets
({ inner = { a = 1; }; } // { inner = { b = 2; }; }) # = { inner = { b = 2; }; }
## doing comparison
a < b
a <= b
a > b
a >= b
a == b
a != b
## doing boolean logic
p && q
p || q
# "p implies q": this is `true` in all cases except when `p` is true and `q` isn't
p -> q
Other Language Constructs
## if expression
# (the else branch is required)
if a then b else c
## binding values to use in an expression
let
a = 1;
b = 2;
in a + b
## binding all the attributes in a set
with {
a = 1;
b = 2;
}; a + b
## inheriting bound values as attributes in a set
let
a = 1;
b = 2;
set = { c = 3; };
in {
inherit a b;
inherit (set) c;
} # = { a = 1; b = 2; c = 3; }
## asserting that a precondition is true
assert true; x # = x
assert false; x # aborts with error "assertion 'false' failed"
# it's common to use the -> operator to express dependency assertions
# e.g. when creating derivations in Nix code
assert enableFoo -> fooPackage != null; /*...*/
## importing other Nix expressions
# (this is actually just a builtin function, but it's a very special one)
{
x = import ./the-value-of-x.nix;
# if you try to import a directory, Nix will instead import the file named
# `default.nix` inside that directory
default-nix = import ./.;
# if you try to import a set, Nix will try to import the attribute `outPath`,
# if present, and fail otherwise
#
# this is meant to be used with derivations, to import their output paths.
from-set = import { outPath = ./.; }
from-drv = import (derivation { /*...*/ })
}
String Conversion
In the process of building derivations, Nix regularly needs to communicate its values to an outside environment through commandline arguments and environment variables. As such, the way the language converts different values to strings is significant enough to get its own section.
## conversions when using string interpolation
# strings don't need converting
"${"a string"}" # = "a string"
# sets can only be interpolated if they contain a conversion function called `__toString`
"${{
foo = "bar";
__toString = self: self.foo;
}}" # = "bar"
# or, alternatively, if they contain an attribute `outPath`, this is used.
# again, this is intended for printing derivations' output paths
"${my-drv}" # = "/nix/store/..."
# paths are *copied to the store* as if by `nix add-path` and then that store path is used
"${/path/to/foo}" # = "/nix/store/<hash>-foo"
# integers, floats, lists, functions, booleans, and null can't be interpolated
## conversions when using the builtin `toString`
# the same as string interpolation, except:
# paths aren't copied
toString /path/to/foo # = "/path/to/foo"
# but relative paths are displayed in absolute form
toString ./foo # = "/path/to/foo" (if your current directory is /path/to)
# integers are displayed as written
toString 10 # = "10"
# floats are displayed with exactly 6 digits after the point, subject to rounding
toString 1.0 # = "1.000000"
toString 1.1234561 # = "1.123456"
toString 1.9999996 # = "2.000000"
# true, false, and null are weird
toString true # = "1"
toString false # = ""
toString null # = ""
# lists are flattened, then each value is converted and joined with spaces
toString [ 3 [ "different" { __toString = _: "types" } ] ] # = "3 different types"
A number of builtin functions not mentioned here are also included. Most of
them are inside a set called builtins
, but some of the most
important ones are also available in the global namespace directly. You can
find a complete listing
in the Nix manual.
Perhaps the most important builtin we haven't talked about is
derivation
, for producing a derivation value from inside the Nix
language.* There's a lot to cover
about writing your own derivations, though, enough to deserve its own page!
Or, if you're more interested in configuring NixOS systems or declaratively managing your dotfiles, you can start here instead!