1
0
Fork 0
mirror of https://github.com/NixOS/nix.dev.git synced 2024-10-18 14:32:43 -04:00
nix.dev/source/tutorials/nix-language.md
Valentin Gagarin 30d76c858b use less ambiguous grammar for what NixOS is
Co-authored-by: Silvan Mosberger <github@infinisil.com>
2022-10-04 08:34:33 +02:00

38 KiB
Raw Blame History

(reading-nix-language)=

Nix language basics

The Nix language is used to declare packages and configurations for Nix.

It is a domain-specific, purely functional, lazily evaluated, dynamically typed programming language.

Notable uses of the Nix language are:

  • Nixpkgs

    The largest, most up-to-date software distribution in the world, and written in Nix language.

  • NixOS

    Linux distribution that can be configured fully declaratively and is based on Nix.

    Its underlying modular configuration system is written in Nix language, and uses packages from Nixpkgs. The operating system environment and services it provides are configured with Nix language.

Overview

Using the Nix language in practice entails multiple things:

  • language: syntax and semantics
  • function libraries: builtins and pkgs.lib
  • developer tools: testing, debugging, linting and formatting, ...
  • generic build mechanisms: stdenv, trivial builders, ...
  • composition and configuration mechanisms: override, overrideAttrs, overlays, callPackage, ...
  • ecosystem-specific packaging mechanisms: buildGoModule, buildPythonApplication, ...
  • NixOS module system: config, option, ...

This guide only covers language syntax and semantics, briefly discusses function libraries, and at the end will direct you to resources on the other components.

What will you learn?

This guide should enable you to read typical Nix language code and understand its structure.

It shows the most common and distingushing patterns in the Nix language:

It does not explain all Nix language features in detail. See the Nix manual for a full language reference.

What do you need?

  • Familiarity with software development
  • Familiarity with Unix shell, to read command line examples
  • A Nix installation to run the examples

How long does it take?

  • No experience with functional programming: 2 hours

    Carefully work through all definitions and examples.

    Make sure you understand them by reading all detailed explanations.

  • Familiar with functional programming: 1 hour

    Learn syntax and semantics by examining definitions and examples, and read detailed explanations if in doubt.

  • Proficient with functional programming: 30 minutes

    The Nix language is largely unsurprizing to experienced functional programmers.

    Read through the guide to get accustomed to the syntax and language features.

We recommend running all examples and playing with them to validate your assumptions and test what you have learned.

How to run the examples?

  • Every valid piece of Nix language code is a Nix expression.
  • Nix expressions can contain other Nix expressions, that is, they can be nested.
  • Evaluating a Nix expression produces a single value.
  • Every Nix file (.nix) contains a single Nix expression.

:::{note} To evaluate means to transform an expression according to the language rules until no further simplification is possible. :::

All examples in this guide are valid Nix expressions. Each one is followed by the expected evaluation result.

The following example is a Nix expression adding two numbers:

1 + 2
3

Interactive evaluation

Use nix repl to evaluate Nix expressions interactively (by typing them on the command line):

nix repl
Welcome to Nix 2.5.1. Type :? for help.
nix-repl> 1 + 2
3

Evaluating Nix files

Use nix-instantiate --eval to evaluate the expression in a Nix file.

echo 1 + 2 > file.nix

nix-instantiate --eval file.nix
3
Detailed explanation

The first command writes 1 + 2 to a file file.nix in the current directory. The contents of file.nix are now 1 + 2, which you can check with

cat file.nix
1 + 2

The second command runs nix-instantiate with the --eval option on file.nix, which reads the file and evaluates the contained Nix expression. The resulting value is printed as output.

--eval is required to evaluate the file and do nothing else. If --eval is omitted, nix-instantiate expects the expression in the given file to evaluate to a special data type called Derivation, which we will come back to at the end of this guide in .

If you do not need file.nix any more, remove it with

rm file.nix

:::{note} nix-instantiate --eval will evaluate default.nix if no file name is specified.

echo 1 + 2 > default.nix

nix-instantiate --eval
3

:::

Reading the Nix language without fear

You will quickly encounter Nix language expressions that may look very complicated.

As with any programming language, the required amount of Nix language code closely matches the complexity of the problem it is supposed to solve, and reflects how well the problem and its solution is understood.

Building software is a complex undertaking, and Nix both exposes and allows managing this complexity with the Nix language.

The purpose of the Nix language is to create build tasks: precise descriptions of how contents of existing files are used to derive new files.

:::{important} A build task in Nix is called derivation. :::

The Nix language has only few basic constructs which can be combined arbitrarily:

  • primitive data types

    as basic building blocks

  • compound data types and functions

    to produce and transform complex data

  • name assignment

    to manipulate complex data as units

In addition it allows three side effects:

  • reading files as Nix expressions

    to enable code reuse

  • reading files as build inputs

    to capture what build tasks will operate on

  • writing files as build tasks

    to keep them for later execution, the build

There is nothing else to it. What may look complicated comes not from the language, but from how it is used.

(names-values)=

Names and values

Values in Nix language can be primitive data types, lists, attribute sets, and functions.

We show primitve data types and lists as examples in the context of attribute sets. Later in this section we cover special features of character strings: file system paths, indented strings, and antiquotation. We deal with functions separately.

There are two ways to assign names to values in Nix: attribute sets and let expressions.

Assignments are denoted by a single equal sign (=).

(attrset)=

Attribute set { ... }

An attribute set is an unordered collection of name-value-pairs, where names must be unique.

The following example shows all primitive data types, lists, and attribute sets.

:::{note} If you are familiar with JSON, imagine the Nix language as JSON with functions.

Nix language data types without functions work just like their counterparts in JSON and look very similar. :::

Nix language JSON
{
  string = "hello";
  integer = 1;
  float = 3.141;
  bool = true;
  null = null;
  list = [ 1 "two" false ];
  attribute-set = {
    a = "hello";
    b = 2;
    c = 2.718;
    d = false;
  }; # comments are supported
}
{
  "string": "hello",
  "integer": 1,
  "float": 3.141,
  "bool": true,
  "null": null,
  "list": [1, "two", false],
  "object": {
    "a": "hello",
    "b": 1,
    "c": 2.718,
    "d": false
  }
}

:::{note}

  • Attribute names usually do not need quotes.1
  • List elements are separated by white space.2 :::

(rec-attrset)=

Recursive attribute set rec { ... }

You will sometimes see attribute sets declared with rec prepended. This allows access to attributes from within the set.

Example:

rec {
  one = 1;
  two = one + 1;
  three = two + 1;
}
{ one = 1; three = 3; two = 2; }

:::{note} Attribute sets are unordered. The evaluator prints them in alphabetic order. :::

Counter-example:

{
  one = 1;
  two = one + 1;
  three = two + 1;
}
error: undefined variable 'one'

       at «string»:3:9:

            2|   one = 1;
            3|   two = one + 1;
             |         ^
            4|   three = two + 1;

{ref}We recommend to avoid recursive sets <rec-expression> and to use the let expression instead.

let ... in ...

Also known as “let expression” or “let binding”

let expressions allow assigning names to values for repeated use.

Example:

let
  a = 1;
in
a + a
2
Detailed explanation

Assignments are placed between the keywords let and in. In this example we assign a = 1.

After in comes the expression in which the assignments are valid, i.e., where assigned names can be used. In this example the expression is a + a, where a refers to a = 1.

By replacing the names with their assigned values, a + a evaluates to 2.

Names can be assigned in any order, and expressions on the right of the assignment (=) can refer to other assigned names.

Example:

let
  b = a + 1;
  a = 1;
in
a + b
3
Detailed explanation

Assignments are placed between the keywords let and in. In this example we assign a = 1 and b = a + 1.

The order of assignments does not matter. Therefore the following example, where the assignments are in reverse order, is equivalent:

let
  a = 1;
  b = a + 1;
in
a + b
3

Note that the a in b = a + 1 refers to a = 1.

After in comes the expression in which the assignments are valid. In this example the expression is a + b, where a refers to a = 1, and b refers to b = a + 1.

By replacing the names with their assigned values, a + b evaluates to 3.

This is similar to recursive attribute sets: in both, the order of assignments does not matter, and names on the left can be used in expressions on the right of the assignment (=).

Example:

let ... in ...

rec { ... }

let
  b = a + 1;
  c = a + b
  a = 1;
in {  c = c; a = a; b = b; }
{ a = 1; b = 2; c = 3; }
rec {
  b = a + 1;
  c = a + b;
  a = 1;
}
{ a = 1; b = 2; c = 3; }

The difference is that while a recursive attribute set evaluates to an attribute set, any expression can follow after the in keyword.

In the following example we use the let expression to form a list:

let
  b = a + 1;
  c = a + b
  a = 1;
in [ a b c ]
[ 1 2 3 ]

Only expressions within the let expression itself can access the newly declared names. We say: the bindings have local scope.

Counter-example:

{
  a = let x = 1; in x;
  b = x;
}
error: undefined variable 'x'

       at «string»:3:7:

            2|   a = let x = 1; in x;
            3|   b = x;
             |       ^
            4| }

Attribute access

Attributes in a set are accessed with a dot (.) and the attribute name.

Example:

let
  attrset = { x = 1; };
in
attrset.x
1

Accessing nested attributes works the same way.

Example:

let
  attrset = { a = { b = { c = 1; }; }; };
in
attrset.a.b.c
1

The dot (.) notation can also be used for assigning attributes.

Example:

let
  attrset = { a.b.c = 1; };
in
attrset
{ a = { b = { c = 1; }; }; }

(with)=

with ...; ...

The with expression allows access to attributes without repeatedly referencing their attribute set.

Example:

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
with a; [ x y z ]
[ 1 2 3 ]

The expression

with a; [ x y z ]

is equivalent to

[ a.x a.y a.z ]

Attributes made available through with are only in scope of the expression following the semicolon (;).

Counter-example:

let
  a = {
    x = 1;
    y = 2;
    z = 3;
  };
in
{
  b = with a; [ x y z ];
  c = x;
}
error: undefined variable 'x'

       at «string»:10:7:

            9|   b = with a; [ x y z ];
           10|   c = x;
             |       ^
           11| }

inherit ...

With inherit one can assign existing names to attributes of the same name. It is for convenience to avoid repeating the same name multiple times.

Example:

let
  x = 1;
  y = 2;
in
{
  inherit x y;
}
{ x = 1; y = 2; }

The fragment

inherit x y;

is equivalent to

x = x; y = y;

It is also possible to inherit names from a specific attribute set with parentheses (inherit (...) ...).

Example:

let
  a = { x = 1; y = 2; };
in
{
  inherit (a) x y;
}
{ x = 1; y = 2; }

The fragment

inherit (a) x y;

is equivalent to

x = a.x; y = a.y;

(file-system-paths)=

File system paths

Nix language offers convenience syntax for file system paths.

Absolute paths always start with a slash (/).

Example:

/absolute/path
/absolute/path

Paths are relative when they contain at least one slash (/) but to not start with one. They evaluate to the path relative to the file containing the expression.

The following examples assume the containing Nix file is in /current/directory (or nix repl is run in /current/directory).

Example:

./relative
/current/directory/relative

Example:

relative/path
/current/directory/relative/path

One dot (.) denotes the same directory. This is typically used to specify the current directory.

Example:

./.
/current/directory

Two dots (..) denote the parent directory.

Example:

../.
/current

Search path

Also known as “angle bracket syntax”.

<nixpkgs>

The value of a named path is a file system path that depends on the contents of the $NIX_PATH environment variable.

In practice, <nixpkgs> points to the file system path of some revision of nixpkgs, the source repository of Nixpkgs. For example, <nixpkgs/lib> points to the subdirectory lib of that file system path.

While you will see many such examples, we recommend to avoid search paths in practice, as they are not fully reproducible.

(indented-strings)=

Indented strings

Also known as “multi-line strings”.

Nix language offers convenience syntax for character strings which span multiple lines that have common indentation.

Indented strings are denoted by double single quotes ('' '').

Example:

''
multi
line
string
''
"multi\nline\nstring"

Equal amounts of prepended white space are trimmed from the result.

Example:

''
  one
   two
    three
''
"one\n two\n  three\n"

(antiquotation)=

Antiquotation ${ ... }

Also known as “string interpolation”.

The value of a Nix expression can be inserted into a character string with the dollar-sign and braces (${ }).

Example:

let
  name = "Nix";
in
"hello ${name}"
"hello Nix"

Only character strings or values that can be represented as a character string are allowed.

Counter-example:

let
  x = 1;
in
"${x} + ${x} = ${x + x}"
error: cannot coerce an integer to a string

       at «string»:4:2:

            3| in
            4| "${x} + ${x} = ${x + x}"
             |  ^
            5|

Antiquotation can be arbitrarily nested.

(This can become hard to read, and we recommend to avoid it in practice.)

Example:

let
  a = "no";
in
"${a + "${a + " ${a}"}"}"
"no no no"

:::{warning} You may encounter strings that use the dollar sign ($) before an assigned name, but no braces ({ }):

These are not antiquotations, but usually denote variables in a shell script.

In such cases, the use of names from the surrounding Nix expression is a coincidence.

Example:

let
  out = "Nix"
in
"echo ${out} > $out"
"echo Nix > $out"

:::

(functions)=

Functions

Functions are everywhere in the Nix language and deserve particular attention.

Single argument

Functions in Nix language can appear in different forms, but always take exactly one argument. Argument and function body are separated by a colon (:).

Wherever you see a colon (:) in Nix language code:

  • on its left is the function argument
  • on its right is the function body.

Nix functions have no names. We say they are anonymous, and call such a function a lambda.

Example:

x: x + 1
<LAMBDA>

The <LAMBDA> indicates the resulting value is an anonymous function.

We can assign functions a name as to any other value.

Example:

let
  f = x: x + 1
in f
<LAMBDA>

Calling functions

Also known as "function application".

Calling a function with an operand means writing the operand after the function.

Example:

(x: x + 1) 1
2

:::{note} Since function and operand are separated by white space, sometimes parantheses (( )) are required to distinguish expressions. :::

Example:

let
  f = x: x.a;
in
f { a = 1; }
1

The above example calls f on a literal attribute set. One can also pass operands by name.

Example:

let
  f = x: x.a;
  v = { a = 1; };
in
f v
1

Chaining arguments

Arguments can be chained by nesting functions.

Such a nested function can be used like a function that takes multiple arguments, but offers additional flexibility.

Example:

x: y: x + y
<LAMBDA>

The above function takes one argument and returns a function y: x + y with x set to the value of the argument.

Example:

let
  f = x: y: x + y;
in
f 1
<LAMBDA>

Applying that function f 1 to another argument yields the inner body x + y (with x set to 1 and y set to the other argument), which can now be fully evaluated.

let
  f = x: y: x + y;
in
f 1 2
3

Attribute set argument

Also known as “keyword arguments” or “destructuring” .

Nix functions can be declared to require an attribute set with specific structure as argument.

This is denoted by listing the expected attribute names separated by commas (,) and enclosed in braces ({ }).

Example:

{a, b}: a + b
<LAMBDA>

The argument defines the exact attributes that have to be in that set. Leaving out or passing additional attributes is an error.

Example:

let
  f = {a, b}: a + b;
in
f { a = 1; b = 2; }
3

Counter-example:

let
  f = {a, b}: a + b;
in
f { a = 1; b = 2; c = 3; }
error: 'f' at (string):2:7 called with unexpected argument 'c'

       at «string»:4:1:

            3| in
            4| f { a = 1; b = 2; c = 3; }
             | ^
            5|

Default attributes

Also known as “default arguments”.

Destructured arguments can have default values for attributes.

This is denoted by separating the attribute name and its default value with a question mark (?).

Attributes in the argument are not required if they have a default value.

Example:

let
  f = {a, b ? 0}: a + b
in
f { a = 1; }
1

Example:

let
  f = {a ? 0, b ? 0}: a + b
in
f { } # empty attribute set
0

Additional attributes

Additional attributes are allowed with an ellipsis (...):

{a, b, ...}: a + b

Unlike the previous counter-example, passing additional arguments will not be an error.

Example:

let
  f = {a, b, ...}: a + b
in
f { a = 1; b = 2; c = 3; }
3

Named attribute argument

Also known as “@ pattern”, “@ syntax”, or “at syntax”.

An attribute set argument can be given a name to be accessible as a whole.

This is denoted by prepending or appending the name to the attribute set argument, separated by the at sign (@).

Example:

{a, b, ...}@args: a + b + args.c
<LAMBDA>

or

args@{a, b, ...}: a + b + args.c
<LAMBDA>

Example:

let
  f = {a, b, ...}@args: a + b + args.c
in
f { a = 1; b = 2; c = 3; }
6

(libraries)=

Function libraries

In addition to the built-in operators, there are two widely used libraries that together can be considered standard for the Nix language. You need to know about both to understand and navigate Nix language code.

We recommend to at least skim them to familiarise yourself with what is available.

builtins

Nix comes with many functions that are built into the language.

:::{note} The Nix manual lists all Built-in Functions, and shows how to use them. :::

These functions are available under the builtins constant.

Example:

builtins.toString

Most of them are implemented in the Nix language interpreter itself, which means they usually execute faster than their equivalents implemented in Nix language.

pkgs.lib

The nixpkgs repository contains an attribute set called lib, which provides a large number of useful functions.

:::{note} The Nixpkgs manual lists all Nixpkgs library functions. :::

These functions are usually accessed through pkgs.lib.

Example:

pkgs.lib.strings.toUpper

(side-effects)=

Side effects

So far we have only covered what we call pure expressions: declaring data and transforming it with functions.

Building software requires interaction with the outside world, called side effects.

There are three side effects in the Nix language that are relevant here:

  1. Importing Nix expressions from other files
  2. Reading files from the file system as build inputs
  3. Writing files to the file system as build tasks

Side effects are performed while evaluating a given impure expression.

Imports

The built-in function import takes a path to a Nix file, and reads it to evaluate the contained Nix expression.

Example:

echo 1 + 2 > file.nix
import ./file.nix
3
Detailed explanation

The preceding shell command writes the contents 1 + 2 to the file file.nix in the current directory.

The above Nix expression refers to this file as ./file.nix. import reads the file as a side effect and evaluates to the contained Nix expression.

It is an error if the file system path does not exist.

After reading file.nix the Nix expression is equivalent to the file contents:

1 + 2
3

Example:

echo "x: x + 1" > file.nix
import ./file.nix 1
2
Detailed explanation

The preceding shell command writes the contents x: x + 1 to the file file.nix in the current directory.

The above Nix expression refers to this file as ./file.nix. import ./file.nix reads the file as a side effect and evaluates to the contained Nix expression.

It is an error if the file system path does not exist.

After reading the file, the Nix expression import ./file.nix is equivalent to the file contents:

(x: x + 1) 1
2

This applies the function x: x + 1 to the argument 1, and therefore evaluates to 3.

:::{note} Parentheses are required to separate function declaration from function application. :::

Build inputs

Build inputs are files that build tasks refer to in their precise description of how to derive new files.

Since Nix language evaluation is otherwise pure, the only way to specify build inputs is explicitly with:

  • file system paths
  • dedicated impure functions.

When run, a build task will only have access to explicitly declared build inputs.

:::{important} Purity is the key to reproducible builds.

It precludes build tasks from referring to files which are not explicitly specified as build inputs. :::

Paths

Whenever a file system path is rendered to a character string with antiquotation, the contents of that file are copied to a special location in the file system, the Nix store, as a side effect.

The evaluated string then contains the Nix store path assigned to that file.

Example:

echo 123 > data
"${./data}"
"/nix/store/h1qj5h5n05b5dl5q4nldrqq8mdg7dhqk-data"
Detailed explanation

The preceding shell command writes the characters 123 to the file data in the current directory.

The above Nix expression refers to this file as ./data and converts the file system path to a string with antiquotation ${ ... }.

Only values that can be represented as a character string are allowed for antiquotation. A file system path is such a value, and its character string representation is the corresponding Nix store path.

The Nix store path is obtained by taking the hash of the file's contents (<hash>) and combining it with the file name (<name>). The file is copied into the Nix store directory /nix/store as a side effect of evaluation:

/nix/store/<hash>-<name>

It is an error if the file system path does not exist.

Fetchers

Files to be used as build inputs do not have to come from the file system.

The Nix language provides built-in impure functions to fetch files over the network during evaluation:

These functions evaluate to a file system path in the Nix store.

Example:

builtins.fetchurl https://github.com/NixOS/nix/archive/7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz
"/nix/store/7dhgs330clj36384akg86140fqkgh8zf-7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz"

Some of them add extra convenience, such as automatically unpacking archives.

Example:

builtins.fetchTarball https://github.com/NixOS/nix/archive/7c3ab5751568a0bc63430b33a5169c5e4784a0ff.tar.gz
"/nix/store/d59llm96vgis5fy231x6m7nrijs0ww36-source"

:::{note} The Nixpkgs manual on Fetchers lists numerous additional library functions to fetch files over the network. :::

It is an error if the network request fails.

(build-tasks)=

Build tasks

A build task in Nix is called derivation.

Build tasks are at the core of both Nix and the Nix language:

  • The Nix language is used to produce build tasks.
  • Nix runs build tasks to produce build results.
  • Build results can in turn be used as inputs for other build tasks.

The Nix language primitive to declare a build task is the built-in impure function derivation.

It is usually wrapped by the Nixpkgs build mechanism stdenv.mkDerivation, which hides much of the complexity involved in non-trivial build procedures.

:::{note} You will probably never encounter derivation in practice. :::

Whenever you see mkDerivation, it denotes something that Nix will eventually build.

Example: a package using mkDerivation

The evaluation result of derivation (and mkDerivation) has the special Nix language data type Derivation. It behaves like an attribute set, except that it can be used in antiquotation.

Example:

let
  pkgs = import <nixpkgs> {};
in "${pkgs.nix}"
"/nix/store/rbyjapdp614xfd0l2wiji700x5s8dssl-nix-2.10.3"

:::{note} Your output may differ. It may produce a different hash or even a different package version.

A derivation's output path is fully determined by its inputs, which in this case come from some version of Nixpkgs.

This is why we recommend to avoid search paths, except in examples intended for illustration only. :::

Detailed explanation

The example imports the Nix expression from the search path <nixpkgs>, and applies the resulting function to an empty attribute set {}. Its output is assigned the name pkgs.

It cannot be inferred from the code above, but pkgs is an attribute set of Derivations. That is, each of its attributes ultimately boils down to a call to derivation.

Converting the attribute pkgs.nix to a string with antiquotation is allowed, as pkgs.nix is a Derivation. The resulting string is the file system path where the build result of that derivation will end up.

Antiquotation on Derivation values is used to refer to other build results as file system paths when declaring new build tasks.

This allows constructing arbitrarily complex compositions of derivations with the Nix language.

:::{important} Nix and Nix language distinguish the following concepts which have similar names:

  • derivation

    A build task in Nix. This is an abstract concept embodied by the following representations.

  • derivation

    A built-in function in Nix language. It evaluates to a Derivation within Nix language and produces a store derivation in the Nix store as a side effect.

    The process of creating a store derivation is called instantiation.

  • Derivation

    A data type in Nix language. It is like an attribute set and can be used in antiquotation.

    The character string representation of a Derivation is the Nix store path of its build result. This Nix store path will contain the build result when the derivation is built.

  • store derivation

    A .drv file in the Nix store. It describes a build task, and is produced as a side effect of evaluating derivation.

    The Nix store path of a store derivation is different from the path of the derivation's build result. The store derivation is only the build task for that build result, not the build result itself. :::

Worked examples

You should now be able to read Nix language code for simple packages and configurations, and come up with similiar explanations of the following examples.

Example:

{ pkgs ? import <nixpkgs> {} }:
let
  message = "hello world";
in
pkgs.mkShell {
  buildInputs = with pkgs; [ cowsay ];
  shellHook = ''
    cowsay ${message}
  '';
}

This example declares a shell environment (which runs the shellHook on initialization).

Explanation:

  • This expression is a function that takes an attribute set as an argument.
  • If the argument has the attribute pkgs, it will be used in the function body. Otherwise, by default, import the Nix expression in the file found on the search path <nixpkgs> (which is a function in this case), call the function with an empty attribute set, and use the resulting value.
  • The name message is bound to the string value "hello world".
  • The attribute mkShell of the pkgs set is a function that is passed an attribute set as argument. Its return value is also the result of the outer function.
  • The attribute set passed to mkShell has the attributes buildInputs (set to a list with one element: the cowsay attribute from pkgs) and shellHook (set to an indented string).
  • The indented string contains an antiquotation, which will expand the value of message to yield "hello world".

Example:

{ config, pkgs, ... }: {

  imports = [ ./hardware-configuration.nix ];

  environment.systemPackages = with pkgs; [ git ];

  # ...

}

This example is (part of) a NixOS configuration.

Explanation:

  • This expression is a function that takes an attribute set as an argument. It returns an attribute set.

  • The argument must at least have the attributes config and pkgs, and may have more attributes.

  • The returned attribute set contains the attributes imports and environment. imports is a list with one element: a path to a file next to this Nix file, called hardware-configuration.nix.

    :::{note} imports is not the impure built-in import, but a regular attribute name! :::

  • environment is itself an attribute set with one attribute systemPackages, which will evaluate to a list with one element: the git attribute from the pkgs set.

  • The config argument is not used.

(mkDerivation-example)= Example:

{ lib, stdenv }:

stdenv.mkDerivation rec {

  pname = "hello";

  version = "2.12";

  src = builtins.fetchTarball {
    url = "mirror://gnu/${pname}/${pname}-${version}.tar.gz";
    sha256 = "1ayhp9v4m4rdhjmnl2bq3cibrbqqkgjbl3s7yk2nhlh8vj3ay16g";
  };

  meta = with lib; {
    license = licenses.gpl3Plus;
  };

}

This example is a (simplified) package declaration from Nixpkgs.

Explanation:

  • This expression is a function that takes an attribute set which must have exactly the attributes lib and stdenv.
  • It returns the result of evaluating the function mkDerivaion, which is an attribute of stdenv, applied to a recursive set.
  • The recursive set passed to mkDerivation uses its own pname and version attributes in the argument to the built-in function fetchTarball.
  • The meta attribute is itself an attribute set, where the license attribute has the value that was assigned to the nested attribute lib.licenses.gpl3Plus.

References

Next steps

To get things done:

  • create reproducible shell environments from a Nix file
  • Garbage Collection remove unused build results from the Nix store

To learn more:

If you worked through the examples, you will have noticed that reading the Nix language reveals the structure of the code, but does not necessarily tell what the code actually means.

Often it is not possible to determine from the code at hand

  • the data type of a named value or function argument.
  • the data type a called function accepts for its operand.
  • which attributes are present in a given attribute set.

Example:

{ x, y, z }: (x y) z.a

How do we know

  • that x will be a function that, given an operand, returns a function?
  • that, given x is a function, y will be an appropriate operand to x?
  • that, given (x y) is a function, z.a will be an appropriate operand to (x y)?
  • that z will be an attribute set at all?
  • that, given z is an attribute set, it will have an attribute a?
  • which data type y and z.a will be?
  • the data type of the end result?

And how does the caller of this function know that it requires an attribute set with attributes x, y, z?

Answering such questions requires knowing the context in which a given expression is supposed to be used.

The Nix ecosystem and code style is driven by conventions. Most names you will encounter in Nix language code come from Nixpkgs:

  • Nix Pills - a detailed explanation of derivations and how the Nix package collection is constructed from first principles

Nixpkgs provides generic build mechanisms that are widely used:

Packages from Nixpkgs can be modified through multiple mechanisms:

  • overrides specifically override and overrideAttrs to modify single packages
  • overlays to produce a custom variant of Nixpkgs with individually modified packages

Different language ecosystems and frameworks have different requirements to accommodating them into Nixpkgs:

The NixOS Linux distribution has a modular configuration system that imposes its own conventions:


  1. Details: Nix manual - attribute naming rules ↩︎

  2. Details: Nix manual - lists ↩︎