Difference between revisions of "Overview of the Nix Language"

From NixOS Wiki
Jump to: navigation, search
m
(add helpful bit about paths and interpolation)
Line 504: Line 504:
 
/data/tmp
 
/data/tmp
 
</syntaxHighlight>
 
</syntaxHighlight>
 +
 +
=== Coercing a relative path with interpolated variables to an absolute path (for imports) ===
 +
 +
Sometimes you need to interpolate the value of a Nix variable into the path for an import, however these will not work:
 +
* <code>./desktop-${desktop}.nix</code>
 +
* <code>"./desktop-${desktop}.nix"</code>
 +
* <code>./. + "./desktop-${desktop}.nix"</code>
 +
 +
Instead, use this construction:
 +
* <code>./. + "./desktop-${desktop}.nix"</code>
 +
 +
As a fuller example:
 +
 +
<syntaxHighlight lang=nix>
 +
let
 +
  desktops = [ "elementary" "gnome" "plasma" "sway" ];
 +
in
 +
{
 +
  config.specialisation =
 +
    pkgs.lib.genAttrs desktops (desktop: {
 +
      configuration = {
 +
        boot.loader.grub.configurationName = "${desktop}";
 +
        imports = [
 +
          (./. + "/desktop-${desktop}.nix")
 +
        ];
 +
      };
 +
    });
 +
}
 +
</syntaxHighlight>
 +
```
  
 
=== Writing update scripts / Referencing a relative path as string ===
 
=== Writing update scripts / Referencing a relative path as string ===

Revision as of 09:07, 25 December 2020

This discussion article covers the syntax, semantics, typing, compilation, tooling and libraries of the Nix Expression Language.

The Nix expression language is a pure, lazy, functional language. Purity means that operations in the language don't have side-effects (for instance, there is no variable assignment). Laziness means that arguments to functions are evaluated only when they are needed. Functional means that functions are “normal” values that can be passed around and manipulated in interesting ways. The language is not a full-featured, general purpose language. Its main job is to describe packages, compositions of packages, and the variability within packages.

From the Nix manual

The language was designed especially for the Nix Package Manager.

Learning resources

The manual provides a reference of the Nix language. All language constructs you may use in nix are defined here, together with code snippets.

Nix By Example is a step-by-step tutorial. The nix pills also provide a lot of insight into the language and functional package management in general. The Nix Tour is an interactive tour that uses the actual package manager to learn you the language by example, in the browser.

Language Paradigms

Lazy

Not all expressions in nixpkgs will be evaluated and instantiated as nix performs evaluation only when needed for a finished output. In the following example abort will never be triggered as the variable it belongs to is unused:

let
  a = abort "will never happen";
  b = "hello";
  c = "world";
in b + c

Functional

Functional Programming is a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions or declarations instead of statements.

See also: [1]

Pure

A pure function is a function where the return value is only determined by its input values, without observable side effects. In Nix, all build operations try to be as pure as possible to achieve reproducible builds. This means that wherever you build the packages as few side effects as possible should have an impact onto the build.

Language Features

This section describes the main language features of the nix expression language.

Expressions

When Nix tutorials talk about Nix Expressions they typically mean the definition of a function with multiple inputs which as a result in a derivation. However a Nix expression can be everything, from a simple string, to function to a set of expressions.

Types

The nix language provides a number of basic types:

Type Description Example
Strings Strings either start with double quotes or double single quotes. They also support antiquotation (templating). Leading spaces are stripped with double single quotes. "Say ${pkgs.hello.name}"

Multiline String:

''first line
  second line
''
Integers A whole number without fractional component. 5
Floating-point numbers Decimal numbers. Precision is limited. 1.2
Path Relative paths will become absolute when evaluated, paths must contain a slash. <nixpkgs/pkgs> is also possible and will resolve to the folder incl. subfolders in your NIX_PATH
./hello/world
> /abs/path/to/hello/world
<nixpkgs/lib>
> /path/to/your/nixpkgs/lib
URI Uniform Resource Identifiers http://example.org/foo.tar.bz2
Boolean true, false
Null A representation of nothing. null
Lists Items are separated by space, not comma. Each item can be a value of any other type
[ 1 ./example.bin { hello="world"; }]
Sets Associative data structures. In other languages called dicts(Python),objects(JavaScript) hashes(Ruby) or maps(Java). Essentially a list of key-value pairs
{ key1="value1";  key2="value2"; }
Access values through dot-notation:
{ hello="world"; }.hello
> "world"
Functions See below. argument: function-body

A detailed description of all types can be found in The Nix manual.

Functions

Functions are all unnamed (=lambda) functions with the following notation: argument: nixExpression, e.g. x: x*x.

If you want to give that function a name, you have to assign it to a name, e.g. square = x: x*x. So, f(x) = x*x in math is f = x: x*x in Nix.

If you want to use that function and apply it to a value like f(3), you leave out the parentheses and add a space. So, f(3) in math, is f 3 in Nix.

If you want multiple arguments, you can add arguments like this: arg1: arg2: nixExpression, e.g. f = x: y: x*y. Applying that function to multiple values is easy: f(3,4) in math, is f 3 4 in Nix. If you apply one argument f 3only, a partial function y: 3*yis returned.

Destructuring

In nix it happens that sets are given as arguments to functions. Say that we declare a function which returns the concatenation of attributes a and b of a set like:

concat_a_and_b =  set: set.a + set.b 
 concat_a_and_b { a="hello"; b="world"; }
"helloworld"

It is then possible to destructure the argument set and pick out the attributes that we are interested in, in the function declaration, resulting in a tidier function:

 concat_a_and_b = {a, b}: a + b
concat_a_and_b { a="hello "; b="world"; }
"hello world"

Default argument

It is also possible to assign default values to be used in a function if the caller omits one, but only as part of a set.

add_a_b = { a ? 1, b ? 2 }: a + b
add_a_b {}
3
add_a_b {a=5;}
7

Accepting unexpected attributes in argument set

If you want your function to still run without error if the user provides a set with more attributes than you expected it to have you can use the ellipses.

add_a_b = { a, b }: a + b
add_a_b { a=5; b=2; c=10; }
error: anonymous function at (string):1:2 called with unexpected argument 'c', at (string):1:1
add_a_b = { a, b, ... }: a + b
add_a_b { a=5; b=2; c=10; }
7

You can also store away the arguments in a name of your chosing using the @ pattern.

add_a_b = args@{ a, b, ... }: a + b + args.c
add_a_b { a=5; b=2; c=10; }
17


Operators

Lower precedence means a stronger binding; i.e. this list is sorted from strongest to weakest binding, and in the case of equal precedence between two operators, the associativity decides the binding.

Prec Abbreviation Example Assoc Description
1 SELECT e . attrpath [or def] none Select attribute denoted by the attribute path attrpath from set e. (An attribute path is a dot-separated list of attribute names.) If the attribute doesn’t exist, return default if provided, otherwise abort evaluation.
2 APP e1 e2 left Call function e1 with argument e2.
3 NEG -e none Numeric negation.
4 HAS_ATTR e ? attrpath none Test whether set e contains the attribute denoted by attrpath; return true or false.
5 CONCAT e1 ++ e2 right List concatenation.
6 MUL e1 * e2 left Numeric multiplication.
6 DIV e1 / e2 left Numeric division.
7 ADD e1 + e2 left Numeric addition, or string concatenation.
7 SUB e1 - e2 left Numeric subtraction.
8 NOT !e left Boolean negation.
9 UPDATE e1 // e2 right Return a set consisting of the attributes in e1 and e2 (with the latter taking precedence over the former in case of equally named attributes).
10 LT e1 < e2 left Less than.
10 LTE e1 <= e2 left Less than or equal.
10 GT e1 > e2 left Greater than.
10 GTE e1 >= e2 left Greater than or equal.
11 EQ e1 == e2 none Equality.
11 NEQ e1 != e2 none Inequality.
12 AND e1 && e2 left Logical AND.
13 OR e1 || e2 left Logical OR.
14 IMPL e1 -> e2 none Logical implication (equivalent to !e1 || e2).

Source: Gist of joepie91

Imports

import loads, parses and imports the nix expression stored in path. This keyword is essentially a builtin of nix but not a part of the language itself.

Usage:

  x = import <nixpkgs> {};
  y = trace x.pkgs.hello.name x;

Notable constructs

Nix looks a lot like JSON with functions but also provides a number of very specialized constructs which can help you build clean and easy to read expressions. In this sub-chapter the most notable constructs will be shown by example:

with statement

The with statement introduces the lexical scope of a set into the expression which follows. This means that it brings all keys within that set into scope in that expression. So, you don't need to use the dot notation. Common usages are: On top of expressions:

Look at the following lib set:

lib = { 
  ...
  types={
    attrsOf = ...;
    listOf = ...;
    str = ...;
  }; 
   ...
}

You will see the with statement a lot at the beginning of expression definition. Most of the time it is used to load the lib functions into the namespace for quick access.

{lib, ... }:

with lib;
{
  options = {
    networking.hosts = mkOption {
      type = with types; attrsOf ( listOf str);
      default = {};
    };
  };
  ...  
}

instead of:

{lib, ... }:
{
  options = {
    networking.hosts = lib.mkOption {
      type = lib.types.attrsOf ( lib.types.listOf lib.types.str);
      default = {};
    };
  };
  ...  
}

In package input definitions:

{pkgs}:
{
  ...
  buildInputs = with pkgs; [ curl php coreutils procps ffmpeg ];
}

Instead of :

{pkgs}:
{
  ...
  buildInputs = [ pkgs.curl pkgs.php pkgs.coreutils pkgs.procps pkgs.ffmpeg ];
}

In the package meta tag:

{stdenv, ...}:
{
  ...
  meta = with stdenv.lib; {
    license = with licenses; [ lgp3 gpl3 ];
    maintainers = with maintainers; [ adisbladis lassulus ];
  };
}

Instead of :

{stdenv, ...}:
{
  ...
  meta = {
    license = [ stdenv.lib.licenses.lgp3 stdenv.lib.licenses.gpl3 ];
    maintainers = [ stdenv.lib.maintainers.adisbladis stdenv.lib.maintainers.lassulus ];
  };
}

In a default.nix of an external package:

with import <nixpkgs> {};
stdenv.mkDerivation rec {
    name = "mytool-env";
    src = ./.;
    buildInputs = with pkgs;[
      python34
      python34Packages.docopt
    ];

    shellHook =''
      export HISTFILE=$PWD/histfile
    '' ;
}

let ... in statement

With let you can define local variables which can also reference to self without the need of the rec construct. This feature is used inside expressions to prepare variables which become part of an output. The usage of let is comparable to the Haskell let expression

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

inherit statement

The inherit expression can be used to copy variables from the surrounding lexical scope. A typical use case is to declare the version or name of a derivation in the expression and reuse this parameter in the function to fetch the source.

This is a typical python package derivation as the fetchPypi function also requires pname and version as input:

buildPythonPackage rec {
  pname = "hello";
  version = "1.0";
  src = fetchPypi {
    inherit pname version;
   sha256 = "01ba..0";
  };
}

rec statement

The rec expression turns a basic set into a set where self-referencing is possible. This can be used when the let expression would create too much clutter. It is often seen in package derivation descriptions.

Sample usage:

rec {
  x = y - 100;
  y = 123;
}.x
=> 23

Development tools

Syntax highlighting & editor modes

Nix language has decent syntax highlighting (SH) support among popular code editors, but refactoring/autocomplete is still rare.

Reference: Editor Modes for Nix Files

Tips & Tricks

Finding the definition of a function or package

Nix is often criticized that it has no working “jump to definition”. The good news is that you can have something very similar by using a regular expression:

If your package is named hello, searching for the regular expression hello\ = lists all nix symbol definitions that are named this way. In many cases there’s only two or three results. You should find what you are searching for easily.

This trick even works for functions, because their arguments come *after* the equals sign. If you search for mkDerivation\ = for example, you will see that there is more than one definition of that symbol in nixpkgs, but at least all definitions are shown.

You will also notice that searching with grep takes quite a while on a large repository like nixpkgs. Tools like ag (The Silver Searcher) and rg (ripgrep) are orders of magnitudes faster (especially on modern SSDs).

If you don’t have a nixpkgs checkout at hand, you can use the repo search at search.nix.gsc.io. This even searches in all repositories of the NixOS Github organization.

Another trick that only works for functions, is evaluating the function on the nix repl:

nix-repl> pkgs.lib.strings.makeBinPath
«lambda @ /home/user/nixpkgs/lib/strings.nix:94:42»

This doesn't work for non-functions or builtin functions, which show «primop». It will always find the actual lambda, not an attribute that reexports a partial application, for example.

Convert a string to an (import-able) path

nix-repl> "/home/bernd/folder"
"/home/bernd/folder"

nix-repl> :t "/home/bernd/folder"
a string

nix-repl> builtins.toPath "/home/bernd/folder"
"/home/bernd/folder"

nix-repl> :t builtins.toPath "/home/bernd/folder"
a string

nix-repl> /. + builtins.toPath "/home/bernd/folder"
/home/bernd/folder

nix-repl> :t /. + builtins.toPath "/home/bernd/folder"
a path

In contrast to what builtins.toPath suggests, it does not result in a path, but only checks whether the string is an absolute path, and normalizes it. The trick is to prepend the /. (“root”) path literal, which converts the result to a nix path (that will be copied to the store when used in a derivation).

Be careful not to confuse it with ./., which is the “directory of the current nix file” path literal, and will result in something like /my/scripts/folder/home/bernd/folder (provided you are in /my/scripts/folder).

This trick might be helpful in combination with builtins.getEnv, which returns a string (which might be a path). Be careful, depending on environment variables introduces heavy non-determinism and might lead to rebuilds!

If you need to build a path from a mix of paths and strings variables, you can concatenate strings and paths, but you need to be careful of the evaluation order because Nix removes trailing /.

For example if you need to concatenate /data with a variable call my_var you need to add parenthesis:

nix-repl> let my_var = "tmp"; in /data + "/" + my_var  # WRONG
/datatmp

nix-repl> let my_var = "tmp"; in /data + ("/" + my_var) # Better :)
/data/tmp

Coercing a relative path with interpolated variables to an absolute path (for imports)

Sometimes you need to interpolate the value of a Nix variable into the path for an import, however these will not work:

  • ./desktop-${desktop}.nix
  • "./desktop-${desktop}.nix"
  • ./. + "./desktop-${desktop}.nix"

Instead, use this construction:

  • ./. + "./desktop-${desktop}.nix"

As a fuller example:

let
  desktops = [ "elementary" "gnome" "plasma" "sway" ];
in
{
  config.specialisation = 
    pkgs.lib.genAttrs desktops (desktop: {
      configuration = {
        boot.loader.grub.configurationName = "${desktop}";
        imports = [
          (./. + "/desktop-${desktop}.nix")
        ];
      };
    });
}

```

Writing update scripts / Referencing a relative path as string

Nix has relative path syntax that describes files relative to the current nix file, for example

with import <nixpkgs> {};
let textdata = ../foo.txt;
in runCommand "alldata" {} ''
  echo "=this is a header=" >> $out
  cat ${textdata} >> $out
''

If the file ../foo.txt are needed by evaluation, it is copied to the nix store first, so the script in the resulting drv file looks like this:

"echo \"=this is a header=\" >> $out\ncat /nix/store/dcaph3ib0vq0c27bqzw2vhrakk272mga-foo.txt >> $out\n"

Notice the /nix/store path of foo.txt. When we build the file:

$ nix-build code.nix
these derivations will be built:
  /nix/store/bfv13hxqlwll398y5vi3wn44raw48yva-alldata.drv
building '/nix/store/bfv13hxqlwll398y5vi3wn44raw48yva-alldata.drv'...
/nix/store/9fav4aw2fs8ybaj06gg6cjzz7bkqf461-alldata

$ cat /nix/store/9fav4aw2fs8ybaj06gg6cjzz7bkqf461-alldata
=this is a header=
this
is
some
data

Now, what if we don’t want to import the data file into the store, but still reference the absolute path of that file? We use toString:

with import <nixpkgs> {};
let textdata = toString ../foo.txt;
in writeScript "update-foo.sh" ''
  echo "updating foo.txt!"
  cat "additional new data" >> ${lib.escapeShellArg textdata}
''

In this example we use the actual absolute path of the file to write a script (notice the change from runCommand to writeScript, which are both helper functions from nixpkgs). This script can update the foo.txt file when it is run by bash:

$ cat $(nix-build code.nix)
echo "updating foo.txt!"
echo "additional new data" >> '/home/philip/tmp/foo.txt'

$ bash $(nix-build code.nix)
updating foo.txt!

$ cat foo.txt
this
is
some
data
additional new data

Bear in mind that this makes the absolute path vary between different systems. The users Bob and Alice are going to get different scripts, because the paths of their home folders differ: /home/bob/foo.txt and /home/alice/foo.txt; so it’s not reproducible.

We can use this trick to update the sources of nix expressions (for example by generating a script which updates a json file with the software’s hashes).