Nix Pills
Nix Pills
Preface
1. Why You Should Give it a Try
1.1. Introduction
1.2. Rationale for this series
1.3. Not being purely functional
1.4. Being purely functional
1.5. Mutable vs. immutable
1.6. Conclusion
1.7. Next pill...
2. Install on Your Running System
2.1. Installation
2.2. The beginnings of the Nix store
2.3. The Nix database
2.4. The first profile
2.5. Nixpkgs expressions
2.6. FAQ: Can I change /nix to something else?
2.7. Conclusion
2.8. Next pill...
3. Enter the Environment
3.1. Enter the environment
3.2. Install something
3.3. Path merging
3.4. Rolling back and switching generation
3.5. Querying the store
3.6. Closures
3.7. Dependency resolution
3.8. Recovering the hard way
3.9. Channels
3.10. Conclusion
3.11. Next pill
4. The Basics of the Language
4.1. Value types
4.2. Identifier
4.3. Strings
4.4. Lists
4.5. Attribute sets
4.6. If expressions
4.7. Let expressions
4.8. With expression
4.9. Laziness
4.10. Next pill
5. Functions and Imports
5.1. Nameless and single parameter
5.2. More than one parameter
5.3. Arguments set
5.4. Default and variadic attributes
5.5. Imports
5.6. Next pill
6. Our First Derivation
6.1. The derivation function
6.2. Digression about .drv files
6.3. What's in a derivation set
6.4. Referring to other derivations
6.5. An almost working derivation
6.6. When is the derivation built
6.7. Conclusion
6.8. Next pill
7. Working Derivation
7.1. Introduction
7.2. Using a script as a builder
7.3. The builder environment
7.4. The .drv contents
7.5. Packaging a simple C program
7.6. Explanation
7.7. Enough of nix repl
7.8. Next pill
8. Generic Builders
8.1. Packaging GNU hello world
8.2. A generic builder
8.3. A more convenient derivation function
8.4. Conclusion
8.5. Next pill
9. Automatic Runtime Dependencies
9.1. Build dependencies
9.2. Digression about NAR files
9.3. Runtime dependencies
9.4. Another phase in the builder
9.5. Conclusion
9.6. Next pill
10. Developing with nix-shell
10.1. What's nix-shell
10.2. A builder for nix-shell
10.3. Conclusion
10.4. Next pill
11. Garbage Collector
11.1. How does it work
11.2. Playing with the GC
11.3. Indirect roots
11.4. Cleanup everything
11.5. Conclusion
11.6. Next pill
12. Inputs Design Pattern
12.1. Repositories in Nix
12.2. The single repository pattern
12.3. Packaging graphviz
12.4. Digression about gcc and ld wrappers
12.5. Completing graphviz with gd
12.6. The repository expression
12.7. The inputs pattern
12.8. Conclusion
12.9. Next pill
13. Callpackage Design Pattern
13.1. The callPackage convenience
13.2. Implementing callPackage
13.3. Use callPackage to simplify the repository
13.4. Conclusion
13.5. Next pill
14. Override Design Pattern
14.1. About composability
14.2. The override pattern
14.3. The override implementation
14.4. Conclusion
14.5. Next pill
15. Nix Search Paths
15.1. The NIX_PATH
15.2. Fake it a little
15.3. The path to repository
15.4. A big word about nix-env
15.5. Conclusion
15.6. Next pill
16. Nixpkgs Parameters
16.1. The default.nix expression
16.2. The system parameter
16.3. The config parameter
16.4. About .nix functions
16.5. Conclusion
16.6. Next pill
17. Nixpkgs Overriding Packages
17.1. Overriding a package
17.2. In an imperative world...
17.3. Fixed point
17.3.1. Overriding a set with fixed point
17.4. Overriding nixpkgs packages
17.5. The ~/.nixpkgs/config.nix file
17.6. Conclusion
17.7. Next pill
18. Nix Store Paths
18.1. Source paths
18.1.1. Step 1, compute the hash of the file
18.1.2. Step 2, build the string description
18.1.3. Step 3, compute the final hash
18.2. Output paths
18.3. Fixed-output paths
18.4. Conclusion
18.5. Next pill
19. Fundamentals of Stdenv
19.1. What is stdenv
19.2. The setup file
19.3. How is the setup file built
19.4. The stdenv.mkDerivation builder
19.5. Conclusion
19.6. Next pill...
20. Basic Dependencies and Hooks
20.1. The buildInputs Attribute
20.2. The propagatedBuildInputs Attribute
20.3. Setup Hooks
20.4. Environment Hooks
20.5. Next pill...
Nix Pills
Version 1234-abcdef
Preface
This is a ported version of the Nix Pills, a series of blog posts written by
Luca Bruno (aka Lethalman) and orginally published in 2014 and 2015. It
provides a tutorial introduction into the Nix package manager and Nixpkgs
package collection, in the form of short chapters called 'pills'.
Since the Nix Pills are considered a classic introduction to Nix, an effort to
port them to the current format was led by Graham Christensen (aka grahamc
/ gchristensen) and other contributors in 2017.
Note
There's a lot of documentation that describes what Nix, NixOS and related
projects are. But the purpose of this post is to convince you to give Nix a try.
Installing NixOS is not required, but sometimes I may refer to NixOS as a
real world example of Nix usage for building a whole operating system.
This series aims to complement the existing explanations from the more
formal documents.
The following is a description of Nix. Just as with pills, I'll try to be as short
as possible.
So while in theory it's possible with some current systems to install multiple
versions of the same package, in practice it's very painful.
Let's say you need an nginx service and also an nginx-openresty service. You
have to create a new package that changes all the paths to have, for example,
an -openresty suffix.
Or suppose that you want to run two different instances of mysql: 5.2 and 5.5.
The same thing applies, plus you have to also make sure the two mysqlclient
libraries do not collide.
This is not impossible but it is very inconvenient. If you want to install two
whole stacks of software like GNOME 3.10 and GNOME 3.12, you can
imagine the amount of work.
From an administrator's point of view: you can use containers. The typical
solution nowadays is to create a container per service, especially when
different versions are needed. That somewhat solves the problem, but at a
different level and with other drawbacks. For example, needing orchestration
tools, setting up a shared cache of packages, and new machines to monitor
rather than simple services.
From a developer's point of view: you can use virtualenv for python, or
jhbuild for gnome, or whatever else. But then how do you mix the two
stacks? How do you avoid recompiling the same thing when it could instead
be shared? Also you need to set up your development tools to point to the
different directories where libraries are installed. Not only that, there's the
risk that some of the software incorrectly uses system libraries.
And so on. Nix solves all this at the packaging level and solves it well. A
single tool to rule them all.
What that means is that there's no /bin/bash, there's only that self-contained
build output in the store. The same goes for coreutils and everything else. To
make them convenient to use from the shell, Nix will arrange for binaries to
appear in your PATH as appropriate.
In fact, there's no ldconfig cache either. So where does bash find libc?
$ ldd `which bash`
libc.so.6 => /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/libc
It turns out that when bash was built, it was built against that specific version
of glibc in the Nix store, and at runtime it will require exactly that glibc
version.
Don't be confused by the version in the derivation name: it's only a name for
us humans. You may end up having two derivations with the same name but
different hashes: it's the hash that really matters.
What does all this mean? It means that you could run mysql 5.2 with glibc-
2.18, and mysql 5.5 with glibc-2.19. You could use your python module with
python 2.7 compiled with gcc 4.6 and the same python module with python 3
compiled with gcc 4.8, all in the same system.
From an administrator's point of view: if you want an old PHP version for
one application, but want to upgrade the rest of the system, that's not painful
any more.
From a developer's point of view: if you want to develop webkit with llvm
3.4 and 3.3, that's not painful any more.
Since Nix derivations are immutable, upgrading a library like glibc means
recompiling all applications, because the glibc path to the Nix store has been
hardcoded.
So how do we deal with security updates? In Nix we have some tricks (still
pure) to solve this problem, but that's another story.
Another problem is that unless software has in mind a pure functional model,
or can be adapted to it, it can be hard to compose applications at runtime.
Let's take Firefox for example. On most systems, you install flash, and it
starts working in Firefox because Firefox looks in a global path for plugins.
In Nix, there's no such global path for plugins. Firefox therefore must know
explicitly about the path to flash. The way we handle this problem is to wrap
the Firefox binary so that we can setup the necessary environment to make it
find flash in the nix store. That will produce a new Firefox derivation: be
aware that it takes a few seconds, and it makes composition harder at
runtime.
There are no upgrade/downgrade scripts for your data. It doesn't make sense
with this approach, because there's no real derivation to be upgraded. With
Nix you switch to using other software with its own stack of dependencies,
but there's no formal notion of upgrade or downgrade when doing so.
If there is a data format change, then migrating to the new data format
remains your own responsibility.
1.6. Conclusion
Nix lets you compose software at build time with maximum flexibility, and
with builds being as reproducible as possible. Not only that, due to its nature
deploying systems in the cloud is so easy, consistent, and reliable that in the
Nix world all existing self-containment and orchestration tools are deprecated
by NixOps.
That may sound scary, however after running NixOS on both a server and a
laptop desktop, I'm very satisfied so far. Some of the architectural problems
just need some man-power, other design problems still need to be solved as a
community.
Now we'll install Nix on our running system and understand what changed in
our system after the installation. If you're using NixOS, Nix is already
installed; you can skip to the next pill.
Installing Nix is as easy as installing any other package. It will not drastically
change our system, it will stay out of our way.
2.1. Installation
To install Nix, run curl https://nixos.org/nix/install | sh as a non-root user
and follow the instructions. Alternatively, you may prefer to download the
installation script and verify its integrity using GPG signatures. Instructions
for doing so can be found here: https://nixos.org/nix/download.html.
These articles are not a tutorial on using Nix. Instead, we're going to walk
through the Nix system to understand the fundamentals.
The first thing to note: derivations in the Nix store refer to other derivations
which are themselves in the Nix store. They don't use libc from our system
or anywhere else. It's a self-contained store of all the software we need to
bootstrap up to any particular package.
Note
In a multi-user installation, such as the one used in NixOS, the store is owned
by root and multiple users can install and build software through a Nix
daemon. You can read more about multi-user installations here:
https://nixos.org/nix/manual/#ssec-multi-user.
2.2. The beginnings of the Nix store
Start looking at the output of the install command:
copying Nix to /nix/store..........................
That's the /nix/store we were talking in the first article. We're copying in
the necessary software to bootstrap a Nix system. You can see bash, coreutils,
the C compiler toolchain, perl libraries, sqlite and Nix itself with its own
tools and libnix.
You may have noticed that /nix/store can contain not only directories, but
also files, still always in the form hash-name.
Yes, Nix also has a database. It's stored under /nix/var/nix/db. It is a sqlite
database that keeps track of the dependencies between derivations.
The schema is very simple: there's a table of valid paths, mapping from an
auto increment integer to a store path.
Then there's a dependency relation from path to paths upon which they
depend.
You can inspect the database by installing sqlite (nix-env -iA sqlite -f
'<nixpkgs>') and then running sqlite3 /nix/var/nix/db/db.sqlite.
Note
If this is the first time you're using Nix after the initial installation, remember
you must close and open your terminals first, so that your shell environment
will be updated.
Important
Generations can be switched and rolled back atomically, which makes them
convenient for managing changes to your system.
That nix-2.1.3 derivation in the Nix store is Nix itself, with binaries and
libraries. The process of "installing" the derivation in the profile basically
reproduces the hierarchy of the nix-2.1.3 store derivation in the profile by
means of symbolic links.
The contents of this profile are special, because only one program has been
installed in our profile, therefore e.g. the bin directory points to the only
program which has been installed (Nix itself).
But that's only the contents of the latest generation of our profile. In fact,
~/.nix-profile itself is a symbolic link to
/nix/var/nix/profiles/default.
Nix expressions are used to describe packages and how to build them.
Nixpkgs is the repository containing all of the expressions:
https://github.com/NixOS/nixpkgs.
Don't worry about Nix expressions yet, we'll get to them later.
You can see for yourself, don't worry if you see multiple bash derivations:
$ ldd /nix/store/*bash*/bin/bash
[...]
Keeping the store in /nix means we can grab the binary cache from nixos.org
(just like you grab packages from debian mirrors) otherwise:
So the binary cache can't help, because we need a different bash, and so
we'd have to recompile everything ourselves.
2.7. Conclusion
We've installed Nix on our system, fully isolated and owned by the nix user
as we're still coming to terms with this new system.
We learned some new concepts like profiles and channels. In particular, with
profiles we're able to manage multiple generations of a composition of
packages, while with channels we're able to download binaries from
nixos.org.
The installation put everything under /nix, and some symlinks in the Nix
user home. That's because every user is able to install and use software in her
own environment.
I hope I left nothing uncovered so that you think there's some kind of magic
going on behind the scenes. It's all about putting components in the store and
symlinking these components together.
So, where did hello really get installed? which hello is ~/.nix-
profile/bin/hello which points to the store. We can also list the derivation
paths with nix-env -q --out-path. So that's what those derivation paths are
called: the output of a build.
$ ls -l ~/.nix-profile/bin/
[...]
man -> /nix/store/83cn9ing5sc6644h50dqzzfxcs07r2jn-man-1.6g/bin/man
[...]
nix-env -> /nix/store/ig31y9gfpp8pf3szdd7d4sf29zr7igbr-nix-2.1.3/bin/nix-env
[...]
hello -> /nix/store/58r35bqb4f3lxbnbabq718svq9i2pda3-hello-2.10/bin/hello
[...]
Okay, that's clearer now. nix-env merged the paths from the installed
derivations. which man points to the Nix profile, rather than the system man,
because ~/.nix-profile/bin is at the head of $PATH.
Now nix-env -q does not list man anymore. ls -l `which man` should now be
your system copy.
Enough with the rollback, let's go back to the most recent generation:
$ nix-env -G 3
switching from generation 2 to 3
To query and manipulate the store, there's the nix-store command. We can
do some interesting things, but we'll only see some queries for now.
It may not make sense to you right now, but let's print reverse dependencies
of hello:
$ nix-store -q --referrers `which hello`
/nix/store/58r35bqb4f3lxbnbabq718svq9i2pda3-hello-2.10
/nix/store/fhvy2550cpmjgcjcx5rzz328i0kfv3z3-env-manifest.nix
/nix/store/mp987abm20c70pl8p31ljw1r5by4xwfw-user-environment
Was it what you expected? It turns out that our environments depend upon
hello. Yes, that means that the environments are in the store, and since they
contain symlinks to hello, therefore the environment depends upon hello.
Two environments were listed, generation 2 and generation 3, since these are
the ones that had hello installed in them.
3.6. Closures
The closures of a derivation is a list of all its dependencies, recursively,
including absolutely everything necessary to use that derivation.
$ nix-store -qR `which man`
[...]
Copying all those derivations to the Nix store of another machine makes you
able to run man out of the box on that other machine. That's the base of
deployment using Nix, and you can already foresee the potential when
deploying software in the cloud (hint: nix-copy-closures and nix-store --
export).
With the above command, you can find out exactly why a runtime
dependency, be it direct or indirect, exists for a given derivation.
3.9. Channels
So where are we getting packages from? We said something about this
already in the second article. There's a list of channels from which we get
packages, although usually we use a single channel. The tool to manage
channels is nix-channel.
$ nix-channel --list
nixpkgs http://nixos.org/channels/nixpkgs-unstable
If you're using NixOS, you may not see any output from the above command
(if you're using the default), or you may see a channel whose name begins
with "nixos-" instead of "nixpkgs".
Note
~/.nix-channels is not a symlink to the nix store!
To update the channel run nix-channel --update. That will download the
new Nix expressions (descriptions of the packages), create a new generation
of the channels profile and unpack it under ~/.nix-defexpr/channels.
This is quite similar to apt-get update. (See this table for a rough mapping
between Ubuntu and NixOS package management.)
3.10. Conclusion
We learned how to query the user environment and to manipulate it by
installing and uninstalling software. Upgrading software is also
straightforward, as you can read in the manual (nix-env -u will upgrade all
packages in the environment).
Then we learned how to query the store. We inspected the dependencies and
reverse dependencies of store paths.
We saw how symlinks are used to compose paths from the Nix store, a useful
trick.
A quick analogy with programming languages: you have the heap with all the
objects, that corresponds to the Nix store. You have objects that point to other
objects, those correspond to derivations. This is a suggestive metaphor, but
will it be the right path?
The Nix language is used to write expressions that produce derivations. The
nix-build tool is used to build a derivations from an expression. Even as a
system administrator that wants to customize the installation, it's necessary to
master Nix. Using Nix for your jobs means you get the features we saw in the
previous articles for free.
On the other hand, the same syntax is great for describing packages, so
learning the language itself will pay off when writing package expressions.
Important
Important
Launch nix repl. First of all, Nix supports basic arithmetic operations: +, -,
* and /. (To exit nix repl, use the command :q. Help is available through
the :? command.)
nix-repl> 1+3
4
nix-repl> 7-4
3
nix-repl> 3*2
6
What happened? Recall that Nix is not a general purpose language, it's a
domain-specific language for writing packages. Integer division isn't actually
that useful when writing package expressions. Nix parsed 6/3 as a relative
path to the current directory. To get Nix to perform division instead, leave a
space after the /. Alternatively, you can use builtins.div.
nix-repl> 6/ 3
2
nix-repl> builtins.div 6 3
2
Other operators are ||, && and ! for booleans, and relational operators such as
!=, ==, <, >, <=, >=. In Nix, <, >, <= and >= are not much used. There are also
other operators we will see in the course of this series.
Nix has integer, floating point, string, path, boolean and null simple types.
Then there are also lists, sets and functions. These types are enough to build
an operating system.
Nix is strongly typed, but it's not statically typed. That is, you cannot mix
strings and integers, you must first do the conversion.
Not all urls or paths can be parsed this way. If a syntax error occurs, it's still
possible to fallback to plain strings. Literal urls and paths are convenient for
additional safety.
4.2. Identifier
There's not much to say here, except that dash (-) is allowed in identifiers.
That's convenient since many packages use dash in their names. In fact:
nix-repl> a-b
error: undefined variable `a-b' at (string):1:1
nix-repl> a - b
error: undefined variable `a' at (string):1:1
4.3. Strings
It's important to understand the syntax for strings. When learning to read Nix
expressions, you may find dollars ($) ambiguous, but they are very important
. Strings are enclosed by double quotes ("), or two single quotes ('').
nix-repl> "foo"
"foo"
nix-repl> ''foo''
"foo"
In other languages like Python you can also use single quotes for strings (e.g.
'foo'), but not in Nix.
It's possible to interpolate whole Nix expressions inside strings with the
${...} syntax and only that syntax, not $foo or {$foo} or anything else.
Note: ignore the foo = "strval" assignment, special syntax in nix repl.
As said previously, you cannot mix integers and strings. You need to
explicitly include conversions. We'll see this later: function calls are another
story.
Using the syntax with two single quotes is useful for writing double quotes
inside strings without needing to escape them:
nix-repl> ''test " test''
"test \" test"
nix-repl> ''${foo}''
"strval"
Escaping ${...} within double quoted strings is done with the backslash.
Within two single quotes, it's done with '':
nix-repl> "\${foo}"
"${foo}"
nix-repl> ''test ''${foo} test''
"test ${foo} test"
4.4. Lists
Lists are a sequence of expressions delimited by space (not comma):
nix-repl> [ 2 "foo" true (2+3) ]
[ 2 "foo" true 5 ]
For those reading Nix expressions from nixpkgs: do not confuse attribute sets
with argument sets used in functions.
Yes, you can use strings for to address keys which aren't valid identifiers.
Inside an attribute set you cannot normally refer to elements of the same
attribute set:
nix-repl> { a = 3; b = a+4; }
error: undefined variable `a' at (string):1:10
4.6. If expressions
These are expressions, not statements.
nix-repl> a = 3
nix-repl> b = 4
nix-repl> if a > b then "yes" else "no"
"no"
You can't have only the then branch, you must specify also the else branch,
because an expression must have a value in all cases.
The syntax is: first assign variables, then in, then an expression which can
use the defined variables. The value of the whole let expression will be the
value of the expression after the in.
nix-repl> let a = 3; b = 4; in a + b
7
With let you cannot assign twice to the same variable. However, you can
shadow outer variables:
nix-repl> let a = 3; a = 8; in a
error: attribute `a' at (string):1:12 already defined at (string):1:5
nix-repl> let a = 3; in let a = 8; in a
8
So beware when you want to refer to a variable from the outer scope, but it's
also defined in the current let expression. The same applies to recursive
attribute sets.
That's it, it takes an attribute set and includes symbols from it in the scope of
the inner expression. Of course, only valid identifiers from the keys of the set
will be included. If a symbol exists in the outer scope and would also be
introduced by the with, it will not be shadowed. You can however still refer
to the attribute set:
nix-repl> let a = 10; in with longName; a + b
14
nix-repl> let a = 10; in with longName; longName.a + b
7
4.9. Laziness
Nix evaluates expression only when needed. This is a great feature when
working with packages.
nix-repl> let a = builtins.div 4 0; b = 6; in b
6
Since a is not needed, there's no error about division by zero, because the
expression is not in need to be evaluated. That's why we can have all the
packages defined on demand, yet have access to specific packages very
quickly.
So here we defined a function that takes a parameter x, and returns x*2. The
problem is that we cannot use it in any way, because it's unnamed... joke!
As usual, please ignore the special syntax for assignments inside nix repl.
So, we defined a function x: x*2 that takes one parameter x, and returns x*2.
This function is then assigned to the variable double. Finally we did our first
function call: double 3.
Big note: it's not like many other programming languages where you write
double(3). It really is double 3.
In summary: to call a function, name the variable, then space, then the
argument. Nothing else to say, it's as easy as that.
We defined a function that takes the parameter a, the body returns another
function. This other function takes a parameter b and returns a*b. Therefore,
calling mul 3 returns this kind of function: b: 3*b. In turn, we call the
returned function with 4, and get the expected result.
You don't have to use parenthesis at all, Nix has sane priorities when parsing
the code:
nix-repl> mul = a: b: a*b
nix-repl> mul
«lambda»
nix-repl> mul 3
«lambda»
nix-repl> mul 3 4
12
nix-repl> mul (6+7) (8+9)
221
Much more readable, you don't even notice that functions only receive one
argument. Since the argument is separated by a space, to pass more complex
expressions you need parenthesis. In other common languages you would
write mul(6+7, 8+9).
We stored the function returned by mul 3 into a variable foo, then reused it.
In the second case we defined an arguments set. It's like defining a set, except
without values. We require that the passed set contains the keys a and b. Then
we can use those a and b in the function body directly.
nix-repl> mul = { a, b }: a*b
nix-repl> mul { a = 3; b = 4; c = 6; }
error: anonymous function at (string):1:2 called with unexpected argument `c
nix-repl> mul { a = 3; }
error: anonymous function at (string):1:2 called without required argument `
Only a set with exactly the attributes required by the function is accepted,
nothing more, nothing less.
Also you can allow passing more attributes (variadic) than the expected
ones:
nix-repl> mul = { a, b, ... }: a*b
nix-repl> mul { a = 3; b = 4; c = 2; }
However, in the function body you cannot access the "c" attribute. The
solution is to give a name to the given set with the @-pattern:
nix-repl> mul = s@{ a, b, ... }: a*b*s.c
nix-repl> mul { a = 3; b = 4; c = 2; }
24
That's it, you give a name to the whole parameter with name@ before the set
pattern.
You can pass sets, that adds a whole new layer of flexibility and
convenience.
Disadvantages:
Partial application does not work with argument sets. You have to
specify the whole attribute set, not part of it.
5.5. Imports
The import function is built-in and provides a way to parse a .nix file. The
natural approach is to define each component in a .nix file, then compose by
importing these files.
a.nix:
b.nix:
mul.nix:
a: b: a*b
Yes it's really that simple. You import a file, and it gets parsed as expression.
Note that the scope of the imported file does not inherit the scope of the
importer.
test.nix:
test.nix:
Explaining:
Then we import test.nix, and call the function with that set.
The derivation function receives a set as first argument. This set requires at
least the following three attributes:
name: the name of the derivation. In the nix store the format is hash-
name, that's the name.
system: is the name of the system in which the derivation can be built.
For example, x86_64-linux.
Oh oh, what's that? Did it build the derivation? No it didn't, but it did create
the .drv file. nix repl does not build derivations unless you tell to do so.
.drv files are intermediate files like .o files. The .drv describes how to
build a derivation, it's the bare minimum information.
Both drv paths and out paths are stored in the nix store as you can see.
What's in that .drv file? You can read it, but it's better to pretty print it:
$ nix show-derivation /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-
{
"/nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv": {
"outputs": {
"out": {
"path": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
}
},
"inputSrcs": [],
"inputDrvs": {},
"platform": "mysystem",
"builder": "mybuilder",
"args": [],
"env": {
"builder": "mybuilder",
"name": "myname",
"out": "/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname",
"system": "mysystem"
}
}
}
Ok we can see there's an out path, but it does not exist yet. We never told Nix
to build it, but we know beforehand where the build output will be. Why?
Think, if Nix ever built the derivation just because we accessed it in Nix, we
would have to wait a long time if it was, say, Firefox. That's why Nix let us
know the path beforehand and keep evaluating the Nix expressions, but it's
still empty because no build was ever made.
Important: the hash of the out path is based solely on the input derivations in
the current version of Nix, not on the contents of the build product. It's
possible however to have content-addressable derivations for e.g. tarballs as
we'll see later on.
Many things are empty in that .drv, however I write a summary of the .drv
format for you:
1. The output paths (there can be multiple ones). By default nix creates one
out path called "out".
2. The list of input derivations. It's empty because we are not referring to
any other derivation. Otherwise, there would a list of other .drv files.
3. The system and the builder executable (yes, it's a fake one).
The :b is a nix repl specific command to build a derivation. You can see
more commands with :? . So in the output you can see that it takes the .drv
as information on how to build the derivation. Then it says it's trying to
produce our out path. Finally the error we were waiting for: that derivation
can't be built on our system.
We're doing the build inside nix repl, but what if we don't want to use nix
repl? You can realise a .drv with:
$ nix-store -r /nix/store/z3hhlxbckx4g3n9sw91nnvlkjvyw754p-myname.drv
A step forward: of course, that mybuilder executable does not really exist.
Stop for a moment.
You can guess what builtins.isAttrs does, it returns true if the argument is
a set. While builtins.attrNames returns a list of keys of the given set. Some
kind of reflection, you might say.
That's basically the input we gave to the derivation function. Also d.name,
d.system and d.builder attributes are straight the ones we gave as input.
nix-repl> (d == d.out)
true
So out is just the derivation itself, it seems weird but the reason is that we
only have one output from the derivation. That's also the reason why d.all is
a singleton. We'll see multiple outputs later.
Something interesting is the type attribute. It's "derivation". Nix does add a
little of magic to sets with type derivation, but not that much. To let you
understand, you can create yourself a set with that type, it's a simple set:
nix-repl> { type = "derivation"; }
«derivation ???»
Of course it has no other information, so Nix doesn't know what to say :-) But
you get it, the type = "derivation" is just a convention for Nix and for us
to understand the set is a derivation.
When writing packages, we are interested in the outputs. The other metadata
is needed for Nix to know how to create the drv path and the out path.
Nix does the "set to string conversion" as long as there is the outPath
attribute (much like a toString method in other languages):
nix-repl> builtins.toString { outPath = "foo"; }
"foo"
nix-repl> builtins.toString { a = "b"; }
error: cannot coerce a set to a string, at (string):1:1
Say we want to use binaries from coreutils (ignore the nixpkgs etc.):
nix-repl> :l <nixpkgs>
Added 3950 variables.
nix-repl> coreutils
«derivation /nix/store/1zcs1y4n27lqs0gw4v038i303pb89rw6-coreutils-8.21.drv»
nix-repl> builtins.toString coreutils
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21"
Apart the nixpkgs stuff, just think we added to the scope a series of variables.
One of them is coreutils. It is the derivation of the coreutils package you all
know of from other Linux distributions. It contains basic binaries for
GNU/Linux systems (you may have multiple derivations of coreutils in the
nix store, no worries):
$ ls /nix/store/*coreutils*/bin
[...]
I remind you, inside strings it's possible to interpolate Nix expressions with
${...}:
nix-repl> "${d}"
"/nix/store/40s0qmrfb45vlh6610rk29ym318dswdr-myname"
nix-repl> "${coreutils}"
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21"
That's very convenient, because then we could refer to e.g. the bin/true binary
like this:
nix-repl> "${coreutils}/bin/true"
"/nix/store/8w4cbiy7wqvaqsnsnb3zvabq1cp2zhyz-coreutils-8.21/bin/true"
Another step forward, it executed the builder (bin/true), but the builder did
not create the out path of course, it just exited with 0.
Let's examine the new .drv now that we referred to another derivation:
$ nix show-derivation /nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-
{
"/nix/store/qyfrcd53wmc0v22ymhhd5r6sz5xmdc8a-myname.drv": {
"outputs": {
"out": {
"path": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname"
}
},
"inputSrcs": [],
"inputDrvs": {
"/nix/store/hixdnzz2wp75x1jy65cysq06yl74vx7q-coreutils-8.29.drv": [
"out"
]
},
"platform": "x86_64-linux",
"builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29/b
"args": [],
"env": {
"builder": "/nix/store/qrxs7sabhqcr3j9ai0j0cp58zfnny0jz-coreutils-8.29
"name": "myname",
"out": "/nix/store/ly2k1vswbfmswr33hw0kf0ccilrpisnk-myname",
"system": "x86_64-linux"
}
}
}
Realise/Build time: the .drv from the derivation set is built, first
building .drv inputs (build dependencies). This is achieved with nix-
store -r.
Think of it as of compile time and link time like with C/C++ projects. You
first compile all source files to object files. Then link object files in a single
executable.
In Nix, first the Nix expression (usually in a .nix file) is compiled to .drv,
then each .drv is built and the product is installed in the relative out paths.
6.7. Conclusion
It's that complicated to create a package for Nix? No it's not.
When Nix builds a derivation, it first creates a .drv file from a derivation
expression, and uses it to build the output. It does so recursively for all the
dependencies (inputs). It "executes" the .drv files like a machine. Not much
magic after all.
In this post we continue along the path, by creating a derivation that actually
builds something. Then, we try to package a real program: we compile a
simple C file and create a derivation out of it, given a blessed toolchain.
We don't use hash bangs in builder.sh, because at the time we are writing it
we do not know the path to bash in the nix store. Yes, even bash is in the nix
store, everything is there.
We don't even use /usr/bin/env, because then we lose the cool stateless
property of Nix. Not to mention that PATH gets cleared when building, so it
wouldn't find bash anyway.
In addition, we print out the environment variables during the build process.
We cannot use env for this, because env is part of coreutils and we don't have
a dependency to it yet. We only have bash for now.
Like for coreutils in the previous pill, we get a blessed bash for free from our
magic nixpkgs stuff:
nix-repl> :l <nixpkgs>
Added 3950 variables.
nix-repl> "${bash}"
"/nix/store/ihmkc7z2wqk3bbipfnlh0yjrlfkkgnv6-bash-4.2-p45"
So with the usual trick, we can refer to bin/bash and create our derivation:
nix-repl> d = derivation { name = "foo"; builder = "${bash}/bin/bash"; args
nix-repl> :b d
these derivations will be built:
/nix/store/i76pr1cz0za3i9r6xq518bqqvd2raspw-foo.drv
building '/nix/store/i76pr1cz0za3i9r6xq518bqqvd2raspw-foo.drv'...
declare -x HOME="/homeless-shelter"
declare -x NIX_BUILD_CORES="4"
declare -x NIX_BUILD_TOP="/tmp/nix-build-foo.drv-0"
declare -x NIX_LOG_FD="2"
declare -x NIX_STORE="/nix/store"
declare -x OLDPWD
declare -x PATH="/path-not-set"
declare -x PWD="/tmp/nix-build-foo.drv-0"
declare -x SHLVL="1"
declare -x TEMP="/tmp/nix-build-foo.drv-0"
declare -x TEMPDIR="/tmp/nix-build-foo.drv-0"
declare -x TMP="/tmp/nix-build-foo.drv-0"
declare -x TMPDIR="/tmp/nix-build-foo.drv-0"
declare -x builder="/nix/store/q1g0rl8zfmz7r371fp5p42p4acmv297d-bash-4.4-p19
declare -x name="foo"
declare -x out="/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo"
declare -x system="x86_64-linux"
warning: you did not specify '--add-root'; the result might be removed by th
/nix/store/gczb4qrag22harvv693wwnflqy7lx5pb-foo
$PWD and $TMP clearly show that nix created a temporary build directory
Then $builder, $name, $out, and $system are variables set due to the
.drv file's contents.
And that's how we were able to use $out in our derivation and put stuff in it.
It's like Nix reserved a slot in the nix store for us, and we must fill it.
In terms of autotools, $out will be the --prefix path. Yes, not the make
DESTDIR, but the --prefix. That's the essence of stateless packaging. You
don't install the package in a global common path under /, you install it in a
local isolated path under your nix store slot.
Much like the usual .drv, except that there's a list of arguments in there
passed to the builder (bash) with builder.sh… In the nix store..? Nix
automatically copies files or directories needed for the build into the store to
ensure that they are not changed during the build process and that the
deployment is stateless and independent of the building machine. builder.sh
is not only in the arguments passed to the builder, it's also in the input
derivations.
Given that builder.sh is a plain file, it has no .drv associated with it. The
store path is computed based on the filename and on the hash of its contents.
Store paths are covered in detail in a later pill.
Don't worry too much about where those variables come from yet; let's write
the derivation and build it:
nix-repl> :l <nixpkgs>
nix-repl> simple = derivation { name = "simple"; builder = "${bash}/bin/bash
nix-repl> :b simple
this derivation produced the following outputs:
7.6. Explanation
We added two new attributes to the derivation call, gcc and coreutils. In
gcc = gcc;, the name on the left is the name in the derivation set, and the
name on the right refers to the gcc derivation from nixpkgs. The same applies
for coreutils.
We also added the src attribute, nothing magical — it's just a name, to which
the path ./simple.c is assigned. Like simple-builder.sh, simple.c will be
added to the store.
The trick: every attribute in the set passed to derivation will be converted to
a string and passed to the builder as an environment variable. This is how the
builder gains access to coreutils and gcc: when converted to strings, the
derivations evaluate to their output paths, and appending /bin to these leads
us to their binaries.
The same goes for the src variable. $src is the path to simple.c in the nix
store. As an exercise, pretty print the .drv file. You'll see simple_builder.sh
and simple.c listed in the input derivations, along with bash, gcc and
coreutils .drv files. The newly added environment variables described above
will also appear.
In simple_builder.sh we set the PATH for gcc and coreutils binaries, so that
our build script can find the necessary utilities like mkdir and gcc.
We then create $out as a directory and place the binary inside it. Note that
gcc is found via the PATH environment variable, but it could equivalently be
referenced explicitly using $gcc/bin/gcc.
nix-instantiate : parse and evaluate simple.nix and return the .drv file
corresponding to the parsed derivation set
Afterwards, we call the function with the empty set. We saw this already in
the fifth pill. To reiterate: import <nixpkgs> {} is calling two functions, not
one. Reading it as (import <nixpkgs>) {} makes this clearer.
The value returned by the nixpkgs function is a set. More specifically, it's a
set of derivations. Using the with expression we bring them into scope. This
is equivalent to the :l <nixpkgs> we used in nix repl; it allows us to easily
access derivations such as bash, gcc, and coreutils.
This syntax only makes sense inside sets. There's no magic involved, it's
simply a convenience to avoid repeating the same name for both the attribute
name and the value in scope.
Is it really that hard to package stuff in Nix? No, here we're studying the
fundamentals of Nix.
Chapter 8. Generic Builders
Welcome to the 8th Nix pill. In the previous 7th pill we successfully built a
derivation. We wrote a builder script that compiled a C file and installed the
binary under the nix store.
In this post, we will generalize the builder script, write a Nix expression for
GNU hello world and create a wrapper around the derivation built-in
function.
GNU hello world, despite its name, is a simple yet complete project using
autotools. Fetch the latest tarball here: http://ftp.gnu.org/gnu/hello/hello-
2.10.tar.gz.
Nix on darwin
The darwin (i.e. macOS) stdenv diverges from the Linux stdenv in several
ways. A main difference is that the darwin stdenv relies upon clang rather
than gcc as its C compiler. We can adapt this early example of how a stdenv
works for darwin by using this modified version of hello.nix:
with (import <nixpkgs> {});
derivation {
name = "hello";
builder = "${bash}/bin/bash";
args = [ ./hello_builder.sh ];
inherit gnutar gzip gnumake coreutils gawk gnused gnugrep;
gcc = clang;
binutils = clang.bintools.bintools_bin;
src = ./hello-2.10.tar.gz;
system = builtins.currentSystem;
}
Please note the --prefix=$out we were talking about in the previous pill.
for d in *; do
if [ -d "$d" ]; then
cd "$d"
break
fi
done
./configure --prefix=$out
make
make install
What do we do here?
3. We'll see this below in detail, however for each path in $buildInputs,
we append bin to PATH.
5. Find a directory where the source has been unpacked and cd into it.
As you can see, there's no reference to "hello" in the builder anymore. It still
does several assumptions, but it's certainly more generic.
All clear, except that buildInputs. However it's easier than any black magic
you are thinking in this moment.
Nix is able to convert a list to a string. It first converts the elements to strings,
and then concatenates them separated by a space:
nix-repl> builtins.toString 123
"123"
nix-repl> builtins.toString [ 123 456 ]
"123 456"
Create autotools.nix:
pkgs: attrs:
with pkgs;
let defaultAttrs = {
builder = "${bash}/bin/bash";
args = [ ./builder.sh ];
baseInputs = [ gnutar gzip gnumake gcc binutils-unwrapped coreutils gawk
buildInputs = [];
system = builtins.currentSystem;
};
in
derivation (defaultAttrs // attrs)
Ok now we have to remember a little about Nix functions. The whole nix
expression of this autotools.nix file will evaluate to a function. This
function accepts a parameter pkgs, then returns a function which accepts a
parameter attrs.
The body of the function is simple, yet at first sight it might be hard to grasp:
The // operator is an operator between two sets. The result is the union of the
two sets. In case of conflicts between attribute names, the value on the right
set is preferred.
So we use defaultAttrs as base set, and add (or override) the attributes from
attrs.
We create the derivation specifying only name and src. If the project
eventually needed other dependencies to be in PATH, then we would
simply add those to buildInputs (not specified in hello.nix because
empty).
Note we didn't use any other library. Special C flags may be needed to find
include files of other libraries at compile time, and ld flags at link time.
8.4. Conclusion
Nix gives us the bare metal tools for creating derivations, setting up a build
environment and storing the result in the nix store.
Out of this we managed to create a generic builder for autotools projects, and
a function mkDerivation that composes by default the common components
used in autotools projects instead of repeating them in all the packages we
would write.
We are feeling the way a Nix system grows up: it's about creating and
composing derivations with the Nix language.
Analogy: in C you create objects in the heap, and then you compose them
inside new objects. Pointers are used to refer to other objects.
In Nix you create derivations stored in the nix store, and then you compose
them by creating new derivations. Store paths are used to refer to other
derivations.
Today we stop by the GNU hello world program to analyze build and runtime
dependencies, and enhance the builder in order to avoid unnecessary runtime
dependencies.
For the rationale and implementation details you can find more in the
Dolstra's PhD Thesis.
To create NAR archives, it's possible to use nix-store --dump and nix-store -
-restore. Those two commands work regardless of /nix/store.
There's really black magic involved. It's something that at first glance makes
you think "no, this can't work in the long term", but at the same it works so
well that a whole operating system is built on top of this magic.
Steps:
You get really all the runtime dependencies, and that's why Nix deployments
are so easy.
$ nix-instantiate hello.nix
/nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
$ nix-store -r /nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
/nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello
$ nix-store -q --references /nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hell
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19
/nix/store/8jm0wksask7cpf85miyakihyfch1y21q-gcc-4.8.3
/nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello
Ok glibc and gcc. Well, gcc really should not be a runtime dependency!
$ strings result/bin/hello|grep gcc
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib:/nix/store/8jm0wk
Oh Nix added gcc because its out path is mentioned in the "hello" binary.
Why is that? That's the ld rpath. It's the list of directories where libraries can
be found at runtime. In other distributions, this is usually not abused. But in
Nix, we have to refer to particular versions of libraries, thus the rpath has an
important role.
The build process adds that gcc lib path thinking it may be useful at runtime,
but really it's not. How do we get rid of it? Nix authors have written another
magical tool called patchelf, which is able to reduce the rpath to the paths that
are really used by the binary.
Not only, even after reducing the rpath the hello binary would still depend
upon gcc. Because of debugging information. For that, the well known strip
can be used.
We add a new phase after the installation phase, which we call fixup phase.
At the end of the builder.sh follows:
find $out -type f -exec patchelf --shrink-rpath '{}' \; -exec strip '{}' \;
That is, for each file we run patchelf --shrink-rpath and strip. Note that we
used two new commands here, find and patchelf. Exercise: These two
deserve a place in baseInputs of autotools.nix as findutils and patchelf.
The package is self-contained, copy its closure on another machine and you
will be able to run it. I remind you the very few components under the
/nix/store necessary to run nix when we installed it. The hello binary will
use that exact version of glibc library and interpreter, not the system one:
$ ldd result/bin/hello
linux-vdso.so.1 (0x00007fff11294000)
libc.so.6 => /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/lib
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/ld-linux-x86-64.
9.5. Conclusion
Short post compared to previous ones as I'm still on vacation, but I hope you
enjoyed it. Nix provides tools with cool features. In particular, Nix is able to
compute all runtime dependencies automatically for us. Not only shared
libraries, but also referenced executables, scripts, Python libraries etc..
This makes packages self-contained, because we're sure (apart data and
configuration) that copying the runtime closure on another machine is
sufficient to run the program. That's why Nix has one-click install, or reliable
deployment in the cloud. All with one tool.
In return from vacation, we want to hack a little the GNU hello world
program. The nix-build tool creates an isolated environment for building the
derivation, we want to do the same in order to modify some source files of
the project.
I remind you, in a nix environment you don't have access to libraries and
programs unless you install them with nix-env. However installing libraries
with nix-env is not good practice. We prefer to have isolated environments
for development.
$ nix-shell hello.nix
[nix-shell]$ make
bash: make: command not found
[nix-shell]$ echo $baseInputs
/nix/store/jff4a6zqi0yrladx3kwy4v6844s3swpc-gnutar-1.27.1 [...]
But, we have the environment variables that we set in the derivation, like
$baseInputs, $buildInputs, $src and so on.
That means we can source our builder.sh, and it will build the derivation.
You may get an error in the installation phase, because the user may not have
the permission to write to /nix/store:
[nix-shell]$ source builder.sh
...
We're able to cd into hello-2.10 and type make, because now it's available.
In other words, nix-shell drops us in a shell with the same (or almost)
environment used to run the builder!
First of all, we were able to source builder.sh because it was in our current
directory, but that's not nice. We want the builder.sh that is stored in the nix
store, the one that would be used by nix-build. To do so, the right way is to
pass the usual environment variable through the derivation.
Note: $builder is already defined, but it's the bash executable, not our
builder.sh. Our builder.sh is an argument to bash.
Second, we don't want to run the whole builder, we only want it to setup the
necessary environment for manually building the project. So we'll write two
files, one for setting up the environment, and the real builder.sh that runs
with nix-build.
Additionally, we'll wrap the phases in functions, it may be useful, and move
the set -e to the builder instead of the setup. The set -e is annoying in nix-
shell.
function unpackPhase() {
tar -xzf $src
for d in *; do
if [ -d "$d" ]; then
cd "$d"
break
fi
done
}
function configurePhase() {
./configure --prefix=$out
}
function buildPhase() {
make
}
function installPhase() {
make install
}
function fixupPhase() {
find $out -type f -exec patchelf --shrink-rpath '{}' \; -exec strip '{}' \
}
function genericBuild() {
unpackPhase
configurePhase
buildPhase
installPhase
fixupPhase
}
It's all that straight, nix-shell builds the .drv file and its input dependencies,
then drops into a shell by setting up the environment variables necessary to
build the .drv, in particular those passed to the derivation function.
10.3. Conclusion
With nix-shell we're able to drop into an isolated environment for developing
a project, with the necessary dependencies just like nix-build does, except we
can build and debug the project manually, step by step like you would do in
any other operating system. Note that we did never install gcc, make, etc.
system-wide. These tools and libraries are available per-build.
I bet with dpkg, rpm or anything else, you end up with having some
unnecessary package installed or dangling files. With nix this does not
happen.
How do we determine whether a store path is still needed? The same way
programming languages with a garbage collector decide whether an object is
still alive.
Perfect, if you run it again it won't find anything new to delete, as expected.
What's left in the nix store is everything being referenced from the GC roots.
Now remove it, collect garbage and note that bsd-games is still in the nix
store:
$ nix-env -e bsd-games
uninstalling `bsd-games-2.17'
$ nix-collect-garbage
[...]
$ ls /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17
bin share
That's because the old generation is still in the nix store because it's a GC
root. As we'll see below, all profiles and their generations are GC roots.
Removing a GC root is simple. Let's try deleting the generation that refers to
bsd-games, collect garbage, and note that now bsd-games is no longer in the
nix store:
$ rm /nix/var/nix/profiles/default-9-link
$ nix-env --list-generations
[...]
8 2014-07-28 10:23:24
10 2014-08-20 12:47:16 (current)
$ nix-collect-garbage
[...]
$ ls /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17
ls: cannot access /nix/store/b3lxx3d3ggxcggvjw5n0m1ya1gcrmbyn-bsd-games-2.17
$ ls -l /nix/var/nix/gcroots/auto/
total 8
drwxr-xr-x 2 nix nix 4096 Aug 20 10:24 ./
drwxr-xr-x 3 nix nix 4096 Jul 24 10:38 ../
lrwxrwxrwx 1 nix nix 16 Jul 31 10:51 xlgz5x2ppa0m72z5qfc78b8wlciwvgiz -> /
Don't care about the name of the symlink. What's important is that a symlink
exists that point to /home/nix/result. This is called an indirect GC root.
That is, the GC root is effectively specified outside of
/nix/var/nix/gcroots. Whatever result points to, it will not be garbage
collected.
In the first case, the derivation will be deleted from the nix store, and result
becomes a dangling symlink. In the second case, the derivation is removed as
well as the indirect root in /nix/var/nix/gcroots/auto.
Same goes for profiles. Manipulating the nix-env profile will create further
generations. Old generations refer to old software, thus increasing duplication
in the nix store after an upgrade.
What are the basic steps for upgrading and removing everything old,
including old generations? In other words, do an upgrade similar to other
systems, where they forget everything about the older state:
$ nix-channel --update
$ nix-env -u --always
$ rm /nix/var/nix/gcroots/auto/*
$ nix-collect-garbage -d
First, we download a new version of the nixpkgs channel, which holds the
description of all the software. Then we upgrade our installed packages with
nix-env -u. That will bring us into a fresh new generation with all updated
software.
Then we remove all the indirect roots generated by nix-build: beware, this
will result in dangling symlinks. You may be smarter and also remove the
target of those symlinks.
11.5. Conclusion
Garbage collection in Nix is a powerful mechanism to cleanup your system.
The nix-store commands allows us to know why a certain derivation is in the
nix store.
Nix is a language, and it is powerful enough to let you choose the format of
your own repository. In this sense, it is not declarative, but functional.
There is no preset directory structure or preset packaging policy. It's all about
you and Nix.
The nixpkgs repository has a certain structure, which evolved and evolves
with the time. Like other languages, Nix has its own history and therefore I'd
like to say that it also has its own design patterns. Especially when
packaging, you often do the same task again and again except for different
software. It's inevitable to identify patterns during this process. Some of these
patterns get reused if the community thinks it's a good way to package the
software.
Some of the patterns I'm going to show do not apply only to Nix, but to other
systems of course.
From now on, we will adopt this technique. The natural implementation in
Nix is to create a top-level Nix expression, and one expression for each
package. The top-level expression imports and combines all expressions in a
giant attribute set with name -> package pairs.
But isn't that heavy? It isn't, because Nix is a lazy language, it evaluates only
what's needed! And that's why nixpkgs is able to maintain such a big
software repository in a giant attribute set.
Build with nix-build graphviz.nix and you will get runnable binaries under
result/bin. Notice how we reused the same autotools.nix of hello.nix.
Let's create a simple png:
$ echo 'graph test { a -- b }'|result/bin/dot -Tpng -o test.png
Format: "png" not recognized. Use one of: canon cmap [...]
Oh of course... graphviz can't know about png. It built only the output
formats it supports natively, without using any extra library.
This 2.38 version of graphviz has several plugins to output png. For
simplicity, we will use libgd.
The nixpkgs provides gcc and binutils, and we are using them for our
packaging. Not only, it also provides wrappers for them which allow passing
extra arguments to gcc and ld, bypassing the project build systems:
What can we do about it? We can employ the same trick we did for PATH:
automatically filling the variables from buildInputs. This is the relevant
snippet of setup.sh:
for p in $baseInputs $buildInputs; do
if [ -d $p/bin ]; then
export PATH="$p/bin${PATH:+:}$PATH"
fi
if [ -d $p/include ]; then
export NIX_CFLAGS_COMPILE="-I $p/include${NIX_CFLAGS_COMPILE:+ }$NIX_CFL
fi
if [ -d $p/lib ]; then
export NIX_LDFLAGS="-rpath $p/lib -L $p/lib${NIX_LDFLAGS:+ }$NIX_LDFLAGS
fi
done
Now by adding derivations to buildInputs, will add the lib, include and
bin paths automatically in setup.sh.
The [-rpath] flag in ld is needed because at runtime, the executable must use
exactly that version of the library.
If unneeded paths are specified, the fixup phase will shrink the rpath for us!
Now you can create the png! Ignore any error from fontconfig, especially if
you are in a chroot.
For us nixers, this a good technique, because it abstracts from the file names.
We don't refer to a package by REPO/some/sub/dir/package.nix but by
importedRepo.package (or pkgs.package in our examples).
With nix-build:
$ nix-build default.nix -A hello
[...]
$ result/bin/hello
Hello, world!
The [-A] argument is used to access an attribute of the set from the given .nix
expression.
The [-f] option is used to specify the expression to use, in this case the
current directory, therefore ./default.nix.
The current answer to the above questions is: change the expression to match
your needs (or change the callee to match your needs).
With the inputs pattern, we choose to give another answer: let the user
change the inputs of the expression (or change the caller to pass different
inputs).
The src is also an input but it's pointless to change the source from the caller.
For version bumps, in nixpkgs we prefer to write another expression (e.g.
because patches are needed or different inputs are needed).
mkDerivation {
name = "graphviz";
src = ./graphviz-2.38.0.tar.gz;
buildInputs = if gdSupport then [ gd fontconfig libjpeg bzip2 ] else [];
}
I recall that "{...}: ..." is the syntax for defining functions accepting an
attribute set as argument.
If you wanted to build graphviz with a specific version of gd, it would suffice
to pass gd = ...;.
We bring pkgs into the scope when defining the packages set, which is
very convenient instead of typing everytime "pkgs".
12.8. Conclusion
The "inputs" pattern allows our expressions to be easily customizable
through a set of arguments. These arguments could be flags, derivations, or
whatelse. Our package expressions are functions, don't think there's any
magic in there.
It also makes the expressions independent of the repository. Given that all the
needed information is passed through arguments, it is possible to use that
expression in any other context.
12.9. Next pill
...we will talk about the "callPackage" design pattern. It is tedious to specify
the names of the inputs twice, once in the top-level default.nix, and once in
the package expression. With callPackage, we will implicitly pass the
necessary inputs from the top-level expression.
Chapter 13. Callpackage Design
Pattern
Welcome to the 13th Nix pill. In the previous 12th pill we have introduced
the first basic design pattern for organizing a repository of software. In
addition we packaged graphviz to have at least another package for our little
repository.
The next design pattern worth noting is what I'd like to call the callPackage
pattern. This technique is extensively used in nixpkgs, it's the current
standard for importing packages in a repository.
Repository derivation:
rec {
lib1 = import package1.nix { inherit input1 input2 ...; };
program2 = import package1.nix { inherit inputX inputY lib1 ...; };
}
Where inputs may even be packages in the repository itself (note the rec
keyword). The pattern here is clear, often inputs have the same name of the
attributes in the repository itself. Our desire is to pass those inputs from the
repository automatically, and in case be able to specify a particular argument
(that is, override the automatically passed default argument).
Pass default arguments from the repository set, and let us override those
arguments.
Now we need a set with all the values, let's call it values. And a way to
intersect the attributes of values with the function arguments:
nix-repl> values = { a = 3; b = 5; c = 10; }
nix-repl> builtins.intersectAttrs values (builtins.functionArgs add)
{ a = true; b = false; }
nix-repl> builtins.intersectAttrs (builtins.functionArgs add) values
{ a = 3; b = 5; }
Perfect, note from the example above that the intersectAttrs returns a set
whose names are the intersection, and the attribute values are taken from the
second set.
We're done, we have a way to get argument names from a function, and
match with an existing set of attributes. This is our simple implementation of
callPackage:
We take the argument names of the function and intersect with the set of
all values.
In the code above, I've also shown that the callPackage call is equivalent to
directly calling add a b.
We achieved what we wanted. Automatically call functions given a set of
possible arguments. If an argument is not found in the set, that's nothing
special. It's a function call with a missing parameter, and that's an error
(unless the function has varargs ... as explained in the 5th pill).
Note how easy is to override arguments in the case of graphviz without gd.
But most importantly, how easy it was to merge two repositories: nixpkgs
and our pkgs!
The reader should notice a magic thing happening. We're defining pkgs in
terms of callPackage, and callPackage in terms of pkgs. That magic is
possible thanks to lazy evaluation.
13.4. Conclusion
The "callPackage" pattern has simplified a lot our repository. We're able to
import packages that require some named arguments and call them
automatically, given the set of all packages.
The next design pattern is less necessary but useful in many cases and it's a
good exercise to learn more about Nix.
In Nix we mostly talk about functions that accept inputs in order to return
derivations. In our world we want nice utility functions that are able to
manipulate those structures. These utilities add some useful properties to the
original value, and we must be able to apply more utilities on top of it.
For example let's say we have an initial derivation drv and we want it to be a
drv with debugging information and also to apply some custom patches:
debugVersion (applyPatches [ ./patch1.patch ./patch2.patch ] drv)
The final result will be still the original derivation plus some changes. That's
both interesting and very different from other packaging approaches, which is
a consequence of using a functional language to describe packages.
In our repository we have a set of attributes that import the expressions of the
packages and pass these arguments, getting back a derivation. Let's take for
example the graphviz attribute:
graphviz = import ./graphviz.nix { inherit mkDerivation gd fontconfig libjpe
But we may still be diverging from the original graphviz in the repository.
We would like to avoid specifying the nix expression again, instead reuse the
original graphviz attribute in the repository and add our overrides like this:
mygraphviz = graphviz.override { gd = customgd; };
Note: that .override is not a "method" in the OO sense as you may think.
Nix is a functional language. That .override is simply an attribute of a set.
Note that the function f does not return the plain sum but a set, because of the
contract. You didn't forget already, did you? :-)
The variable res is the result of the function call without any override. It's
easy to see in the definition of makeOverridable. In addition you can see the
new override attribute being a function.
Calling that .override with a set will invoke the original function with the
overrides, as expected.
But: we can't override again! Because the returned set with result 15 does not
have an override attribute!
The solution is simple, the .override function should make the result
overridable again:
rec {
makeOverridable = f: origArgs:
let
origRes = f origArgs;
in
origRes // { override = newArgs: makeOverridable f (origArgs // newArg
}
Please note the rec keyword. It's necessary so that we can refer to
makeOverridable from makeOverridable itself.
14.4. Conclusion
The "override" pattern simplifies the way we customize packages starting
from an existing set of packages. This opens a world of possibilities about
using a central repository like nixpkgs, and defining overrides on our local
machine without even modifying the original package.
Once a new version of the overridden package comes out in the repository,
the customized package will make use of it automatically.
The key in Nix is to find powerful yet simple abstractions in order to let the
user customize his environment with highest consistency and lowest
maintenance time, by using predefined composable components.
Assuming you followed the previous posts, I hope you are now ready to
understand nixpkgs. But we have to find nixpkgs in our system first! So this
is the step: introducing some options and environment variables used by nix
tools.
Who uses NIX_PATH? The nix expressions! Yes, NIX_PATH is not of much use
by the nix tools themselves, rather it's used when writing nix expressions.
In the shell for example, when you execute the command ping, it's being
searched in the PATH directories. The first one found is the one being used.
In nix it's exactly the same, however the syntax is different. Instead of just
typing ping you have to type <ping>. Yes, I know... you are already thinking
of <nixpkgs>. However don't stop reading here, let's keep going.
What's NIX_PATH good for? Nix expressions may refer to an "abstract" path
such as <nixpkgs>, and it's possible to override it from the command line.
For ease we will use nix-instantiate --eval to do our tests. I remind you, nix-
instantiate is used to evaluate nix expressions and generate the .drv files.
Here we are not interested in building derivations, so evaluation is enough. It
can be used for one-shot expressions.
15.2. Fake it a little
It's useless from a nix view point, but I think it's useful for your own
understanding. Let's use PATH itself as NIX_PATH, and try to locate ping (or
another binary if you don't have it).
$ nix-instantiate --eval -E '<ping>'
error: file `ping' was not found in the Nix search path (add it using $NIX_P
$ NIX_PATH=$PATH nix-instantiate --eval -E '<ping>'
/bin/ping
$ nix-instantiate -I /bin --eval -E '<ping>'
/bin/ping
Great. At first attempt nix obviously said could not be found anywhere in the
search path. Note that the [-I] option accepts a single directory. Paths added
with [-I] take precedence over NIX_PATH.
Note in the second case how Nix checks whether the path exists or not.
You may have a different path, depending on how you added channels etc..
Anyway that's the whole point. The <nixpkgs> stranger that we used in our
nix expressions, is referring to a path in the filesystem specified by NIX_PATH.
You can list that directory and realize it's simply a checkout of the nixpkgs
repository at a specific commit (hint: .version-suffix).
The NIX_PATH variable is exported by nix.sh, and that's the reason why I
always asked you to source nix.sh at the beginning of my posts.
You may wonder: then I can also specify a different nixpkgs path to, e.g., a
git checkout of nixpkgs? Yes, you can and I encourage doing that. We'll talk
about this in the next pill.
Let's define a path for our repository, then! Let's say all the default.nix,
graphviz.nix etc. are under /home/nix/mypkgs:
$ export NIX_PATH=mypkgs=/home/nix/mypkgs:$NIX_PATH
$ nix-instantiate --eval '<mypkgs>'
{ graphviz = <code>; graphvizCore = <code>; hello = <code>; mkDerivation = <
Yes, nix-build also accepts paths with angular brackets. We first evaluate the
whole repository (default.nix) and then peek the graphviz attribute.
You may be crippled by this concept at the beginning, you may think nix-env
uses NIX_PATH to find the nixpkgs repository. But that's not it.
So if you run nix-env -i graphviz inside your repository, it will install the
nixpkgs one. Same if you set NIX_PATH to point to your repository.
Oh why did it say there's another derivation named graphviz? Because both
graphviz and graphvizCore attributes in our repository have the name
"graphviz" for the derivation:
$ nix-env -f '<mypkgs>' -qaP
graphviz graphviz
graphvizCore graphviz
hello hello
By default nix-env parses all derivations and use the derivation names to
interpret the command line. So in this case "graphviz" matched two
derivations. Alternatively, like for nix-build, one can use [-A] to specify an
attribute name instead of a derivation name:
$ nix-env -f '<mypkgs>' -i -A graphviz
replacing old `graphviz'
installing `graphviz'
This form, other than being more precise, it's also faster because nix-env
does not have to parse all the derivations.
For completeness: you must install graphvizCore with [-A,] since without
the [-A] switch it's ambiguous.
In summary, it may happen when playing with nix that nix-env peeks a
different derivation than nix-build. In such case you probably specified
NIX_PATH, but nix-env is instead looking into ~/.nix-defexpr.
It may or may not make sense for you, or it's like that for historical reasons,
but that's how it works currently, unless somebody comes up with a better
idea.
15.5. Conclusion
The NIX_PATH variable is the search path used by nix when using the angular
brackets syntax. It's possible to refer to "abstract" paths inside nix expressions
and define the "concrete" path by means of NIX_PATH, or the usual [-I] flag in
nix tools.
In general do not abuse NIX_PATH, when possible use relative paths when
writing your own nix expressions. Of course, in the case of <nixpkgs> in our
repository, that's a perfectly fine usage of NIX_PATH. Instead, inside our
repository itself, refer to expressions with relative paths like ./hello.nix.
We can start diving into the nixpkgs repository, through all the various tools
and design patterns. Please note that also nixpkgs has its own manual,
underlying the difference between the general nix language and the nixpkgs
repository.
Also nixpkgs has its own default.nix, which is the one being loaded when
referring to <nixpkgs>. It does a simple thing: check whether the nix version
is at least 1.7 (at the time of writing this blog post). Then import pkgs/top-
level/all-packages.nix. From now on, we will refer to this set of packages as
pkgs.
The all-packages.nix is then the file that composes all the packages. Note
the pkgs/ subdirectory, while nixos is in the nixos/ subdirectory.
The system parameter, as per comment in the expression, it's the system for
which the packages will be built. It allows for example to install i686
packages on amd64 machines.
The config parameter is a simple attribute set. Packages can read some of its
values and change the behavior of some derivations.
myrelease.nix:
{ system ? builtins.currentSystem }:
Why is it useful? With this parameter it's very easy to select a set of packages
for a particular system. For example:
nix-build -A psmisc --argstr system i686-linux
This will build the psmisc derivation for i686-linux instead of x86_64-linux.
This concept is very similar to multi-arch of Debian.
The setup for cross compiling is also in nixpkgs, however it's a little
contrived to talk about it and I don't know much of it either.
pkgs.psmisc
Nix is able to call the function because the pkgs parameter has a default
value. This allows you to pass a different value for pkgs using the --arg
option.
16.5. Conclusion
We've unleashed the <nixpkgs> repository. It's a function that accepts some
parameters, and returns the set of all packages. Due to laziness, only the
accessed derivations will be built.
You can use this repository to build your own packages as we've seen in the
previous pill when creating our own repository.
Lately I'm a little busy with the NixOS 14.11 release and other stuff, and I'm
also looking toward migrating from blogger to a more coder-oriented
blogging platform. So sorry for the delayed and shorter pills :)
We put the override function in the returned attribute set of the original
function call.
Take for example graphviz. It has an input parameter xlibs. If it's null, then
graphviz will build without X support.
$ nix-repl
nix-repl> :l <nixpkgs>
Added 4360 variables.
nix-repl> :b graphviz.override { xlibs = null; }
It's a function that accepts a function f, calls f result on the result just
returned by f result and returns it. In other words it's f(f(f(....
At first sight, it's an infinite loop. With lazy evaluation it isn't, because the
call is done only when needed.
nix-repl> fix = f: let result = f result; in result
nix-repl> pkgs = self: { a = 3; b = 4; c = self.a+self.b; }
nix-repl> fix pkgs
{ a = 3; b = 4; c = 7; }
Without the rec keyword, we were able to refer to a and b of the same set.
The pkgs function gets called again to get the value of a and b.
The trick is that c is not needed to be evaluated in the inner call, thus it
doesn't go in an infinite loop.
Won't go further with the explanation here. A good post about fixed point and
Nix can be found here.
In the first case we computed pkgs with the overrides, in the second case we
also included the overriden attributes in the result.
Now we can build e.g. asciidocFull and it will automatically use the
overridden graphviz:
nix-repl> pkgs = import <nixpkgs> { config = import ./config.nix; }
nix-repl> :b pkgs.asciidocFull
17.6. Conclusion
We've learned about a new design pattern: using fixed point for overriding
packages in a package set.
The newly built asciidoc will depend on the new graphviz, and old asciidoc
will keep using the old graphviz undisturbed.
Before reading existing derivations, I'd like to talk about store paths and how
they are computed. In particular we are interested in fixed store paths that
depend on an integrity hash (e.g. a sha256), which is usually applied to
source tarballs.
The way store paths are computed is a little contrived, mostly due to
historical reasons. Our reference will be the Nix source code.
I remind you, the simplest derivation you can write has a name, a builder and
the system:
$ nix repl
nix-repl> derivation { system = "x86_64-linux"; builder = ./myfile; name = "
«derivation /nix/store/y4h73bmrc9ii5bxg6i7ck6hsf5gqv8ck-foo.drv»
Note: doing nix-store --add myfile will store the file in the same store path.
Or:
$ nix-store --dump myfile|sha256sum
2bfef67de873c54551d884fdab3055d84d573e654efa79db3c0d7b98883f9ee3
In general, Nix understands two contents: flat for regular files, or recursive
for NAR serializations which can be anything.
It's computed in a similar way to source paths, except that the .drv is hashed
and the type of derivation is output:out. In case of multiple outputs, we may
have different output:<id>.
At the time nix computes the out path, the .drv contains an empty string for
each out path. So what we do is getting our .drv and replacing the out path
with an empty string:
$ cp -f /nix/store/y4h73bmrc9ii5bxg6i7ck6hsf5gqv8ck-foo.drv myout.drv
$ sed -i 's,/nix/store/hs0yi5n5nw6micqhy8l1igkbhqdkzqa1-foo,,g' myout.drv
The myout.drv is the .drv state in which nix is when computing the out path
for our derivation:
$ sha256sum myout.drv
1bdc41b9649a0d59f270a92d69ce6b5af0bc82b46cb9d9441ebc6620665f40b5 myout.drv
$ echo -n "output:out:sha256:1bdc41b9649a0d59f270a92d69ce6b5af0bc82b46cb9d94
$ nix-hash --type sha256 --truncate --base32 --flat myout.str
hs0yi5n5nw6micqhy8l1igkbhqdkzqa1
Then nix puts that out path in the .drv, and that's it.
In case the .drv has input derivations, that is it references other .drv, then
such .drv paths are replaced by this same algorithm which returns an hash.
In other words, you get a final .drv where every other .drv path is replaced by
its hash.
The builder must create the out path and make sure its hash is the same as the
one declared with outputHash.
Let's say our builder should create a file whose contents is mycontent:
$ echo mycontent > myfile
$ sha256sum myfile
f3f3c4763037e059b4d834eaf68595bbc02ba19f6d2a500dce06d124e2cd99bb myfile
nix-repl> derivation { name = "bar"; system = "x86_64-linux"; builder = "non
«derivation /nix/store/ymsf5zcqr9wlkkqdjwhqllgwa97rff5i-bar.drv»
Inspect the .drv and see that it also stored the fact that it's a fixed-output
derivation with sha256 algorithm, compared to the previous examples:
$ nix show-derivation /nix/store/ymsf5zcqr9wlkkqdjwhqllgwa97rff5i-
{
"/nix/store/ymsf5zcqr9wlkkqdjwhqllgwa97rff5i-bar.drv": {
"outputs": {
"out": {
"path": "/nix/store/a00d5f71k0vp5a6klkls0mvr1f7sx6ch-bar",
"hashAlgo": "sha256",
"hash": "f3f3c4763037e059b4d834eaf68595bbc02ba19f6d2a500dce06d124e2c
}
},
[...]
}
It doesn't matter which input derivations are being used, the final out path
must only depend on the declared hash.
Hence, the store path only depends on the declared fixed-output hash.
18.4. Conclusion
There are other types of store paths, but you get the idea. Nix first hashes the
contents, then creates a string description, and the final store path is the hash
of this string.
Also we've introduced some fundamentals, in particular the fact that Nix
knows beforehand the out path of a derivation since it only depends on the
inputs. We've also introduced fixed-output derivations which are especially
used by the nixpkgs repository for downloading and verifying source tarballs.
This time we will instead look into nixpkgs, in particular one of its core
derivation: stdenv.
The stdenv is not a special derivation to Nix, but it's very important for the
nixpkgs repository. It serves as base for packaging software. It is used to pull
in dependencies such as the GCC toolchain, GNU make, core utilities, patch
and diff utilities, and so on. Basic tools needed to compile a huge pile of
software currently present in nixpkgs.
result/nix-support:
propagated-user-env-packages
How can this simple derivation pull in all the toolchain and basic tools
needed to compile packages? Let's look at the runtime dependencies:
$ nix-store -q --references result
/nix/store/3a45nb37s0ndljp68228snsqr3qsyp96-bzip2-1.0.6
/nix/store/a457ywa1haa0sgr9g7a1pgldrg3s798d-coreutils-8.24
/nix/store/zmd4jk4db5lgxb8l93mhkvr3x92g2sx2-bash-4.3-p39
/nix/store/47sfpm2qclpqvrzijizimk4md1739b1b-gcc-wrapper-4.9.3
...
How can it be? The package must be referring to those package somehow. In
fact, they are hardcoded in the /setup file:
$ head result/setup
export SHELL=/nix/store/zmd4jk4db5lgxb8l93mhkvr3x92g2sx2-bash-4.3-p39/bin/ba
initialPath="/nix/store/a457ywa1haa0sgr9g7a1pgldrg3s798d-coreutils-8.24 ..."
defaultNativeBuildInputs="/nix/store/sgwq15xg00xnm435gjicspm048rqg9y6-patche
The stdenv setup file is exactly that. It sets up several environment variables
like PATH and creates some helper bash functions to build a package. I invite
you to read it, it's only 860 lines at the time of this writing.
The hardcoded toolchain and utilities are used to initially fill up the
environment variables so that it's more pleasant to run common commands,
similar to what we did with our builder with baseInputs and buildInputs.
The build with stdenv works in phases. Phases are like unpackPhase,
configurePhase, buildPhase, checkPhase, installPhase, fixupPhase. You
can see the default list in the genericBuild function.
What genericBuild does is just run these phases. Default phases are just
bash functions, you can easily read them.
Every phase has hooks to run commands before and after the phase has been
executed. Phases can be overwritten, reordered, whatever, it's just bash code.
How to use this file? Like our old builder. To test it, we enter a fake empty
derivation, source the stdenv setup, unpack the hello sources and build it:
$ nix-shell -E 'derivation { name = "fake"; builder = "fake"; system = "x86_
nix-shell$ unset PATH
nix-shell$ source /nix/store/k4jklkcag4zq4xkqhkpy156mgfm34ipn-stdenv/setup
nix-shell$ tar -xf hello-2.10.tar.gz
nix-shell$ cd hello-2.10
nix-shell$ configurePhase
...
nix-shell$ buildPhase
...
I unset PATH to further show that the stdenv is enough self-contained to build
autotools packages that have no other dependencies.
Note how stdenv is a derivation but it's also an attribute set which contains
some other attributes, like mkDerivation. Nothing fancy here, just
convenience.
Don't be scared by the with expression. It pulls the nixpkgs repository into
scope, so we can directly use stdenv. It looks very similar to the hello
expression in Pill 8.
It builds, and runs fine:
$ nix-build hello.nix
...
/nix/store/6y0mzdarm5qxfafvn2zm9nr01d1j0a72-hello
$ result/bin/hello
Hello, world!
Also take a look at our old derivation wrapper in previous pills! The builder
is bash (that shell variable), the argument to the builder (bash) is default-
builder.sh, and then we add the environment variable $stdenv in the
derivation which is the stdenv derivation.
It's what we did in Pill 10 to make the derivations nix-shell friendly. When
entering the shell, the setup file only sets up the environment without
building anything. When doing nix-build, it actually runs the build process.
Last bit, the unpackPhase in the setup is used to unpack the sources and enter
the directory, again like we did in our old builder.
19.5. Conclusion
The stdenv is the core of the nixpkgs repository. All packages use the
stdenv.mkDerivation wrapper instead of the raw derivation. It does a bunch
of operations for us and also sets up a pleasant build environment.
nix-build
bash -e default-builder.sh
source $stdenv/setup
genericBuild
That's it, everything you need to know about the stdenv phases is in the setup
file.
Really, take your time to read that file. Don't forget that juicy docs are also
available in the nixpkgs manual.
Note
actualHello = stdenv.mkDerivation {
name = "hello-2.3";
src = fetchurl {
url = mirror://gnu/hello/hello-2.3.tar.bz2;
sha256 = "0c7vijq8y68bpr7g6dh1gny0bff8qq81vnp4ch8pjzvg56wb3js1";
};
};
wrappedHello = stdenv.mkDerivation {
name = "hello-wrapper";
unpackPhase = "true";
installPhase = ''
mkdir -p "$out/bin"
echo "#! ${stdenv.shell}" >> "$out/bin/hello"
echo "exec $(which hello)" >> "$out/bin/hello"
'';
};
in wrappedHello
Notice that the wrappedHello derivation finds the hello binary from the PATH.
This works because stdenv contains something like:
pkgs=""
for i in $buildInputs; do
findInputs $i
done
The addToSearchPath call adds $1/bin to _PATH if the former exists (code
here). Once all the packages in buildInputs have been processed, then
content of _PATH is added to PATH, as follows:
PATH="${_PATH-}${_PATH:+${PATH:+:}}$PATH"
With the real hello on the PATH, the installPhase should hopefully make
sense.
actualHello = stdenv.mkDerivation {
name = "hello-2.3";
src = fetchurl {
url = mirror://gnu/hello/hello-2.3.tar.bz2;
sha256 = "0c7vijq8y68bpr7g6dh1gny0bff8qq81vnp4ch8pjzvg56wb3js1";
};
};
intermediary = stdenv.mkDerivation {
name = "middle-man";
propagatedBuildInputs = [ actualHello ];
unpackPhase = "true";
installPhase = ''
mkdir -p "$out"
'';
};
wrappedHello = stdenv.mkDerivation {
name = "hello-wrapper";
unpackPhase = "true";
installPhase = ''
mkdir -p "$out/bin"
echo "#! ${stdenv.shell}" >> "$out/bin/hello"
echo "exec $(which hello)" >> "$out/bin/hello"
'';
};
in wrappedHello
How does this work? You might think we do something in Nix, but actually
its done not at eval time but at build time in bash. lets look at part of the
fixupPhase of stdenv:
fixupPhase() {
## Elided
## Elided
Setup hooks are the basic building block we have for this. In nixpkgs, a
"hook" is basically a bash callback, and a setup hook is no exception. Let's
look at the last part of findInputs we haven't covered:
findInputs() {
local pkg=$1
This is strictly more general than any of the other mechanisms introduced in
this chapter. For example, try writing a setup hook that has the same effect as
a propagatedBuildInputs entry. One can almost think of this as an escape
hatch around Nix's normal isolation guarantees, and the principle that
dependencies are immutable and inert. We're not actually doing something
unsafe or modifying dependencies, but we are allowing arbitrary ad-hoc
behavior. For this reason, setup-hooks should only be used as a last resort.
As a first step, we can move that logic to a setup hook on the C compiler;
indeed that's just what we do in CC Wrapper. [2] But this pattern comes up
fairly often, so somebody decided to add some helper support to reduce
boilerplate.
anEnvHook() {
local pkg=$1
envHooks+=(anEnvHook)
and if one dependency has that setup hook then all of them will be so echoed.
Allowing dependencies to learn about their sibling dependencies is exactly
what compilers need.
[1] Wecan now be precise and consider what addToEnv does alone the
minimal treatment of a dependency: i.e. a package that is just a dependency
would only have addToEnv applied to it.
[2] It
was called GCC Wrapper in the version of nixpkgs suggested for
following along in this pill; Darwin and Clang support hadn't yet motivated
the rename.