Nix in 10 minutes

06.03.2023

Nix is a package manager, a functional language and a build system. It allows you to use any version of any package whenever you want. Arrange groups of packages into profiles, have an ability to rollback if something broke, write declarative build and environment recipes.

Channels

Channel - a git repo with package definitions, like a package repository

# List channels
$ nix-channel --list
# nixpkgs https://nixos.org/channels/nixpkgs-unstable
#   ↑                         ↑
#  name                      URL

# Add a channel
$ nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
$ nix-channel --update

# Remove a channel
$ nix-channel --remove nixpkgs

Profiles

Profile is a set of installed packages. You can have multiple profiles with different versions of different packages and easily switch between them.

On the picture above you can see how two different profiles contain different versions of Subversion, so they coexist on the same OS.

# Create a new profile
$ mkdir /nix/var/nix/profiles/new
$ nix-env -p /nix/var/nix/profiles/new -i nix

# Install package in a profile
$ nix-env -p /nix/var/nix/profiles/new -iA nixpkgs.firefox
# Install package from the store in a profile
$ nix-env -p /nix/var/nix/profiles/new -i /nix/store/wl8vzvvyzpdahrhb482s7ykdy9rxsgxm-python3-3.10.10
# Install specific version of a package
$ nix-env -p /nix/var/nix/profiles/new -iA wget -f https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz

To install a specific version of a package, you need to provide a nixpkgs revision that contains that exact package version. Here’s a nice website that simplifies getting that revision link: https://lazamar.co.uk/nix-versions

If you don’t specify the profile for nix-env, it will operate on the current user’s profile (installation of a package in the current profile is as simple as nix-env -iA nixpkgs.firefox). Here’s how to switch the current profile btw

$ nix-env -S /nix/var/nix/profiles/new

So all the commands below can be ran on a specific profile if you specify it.

# Show packages installed in the current profile
$ nix-env --query "*"

# Remove a package from the current profile
$ nix-env -e chromium

If you remove a package from a profile, it will stay in the Nix store. To remove unused packages (packages that are not used in any profiles), run

$ nix-collect-garbage

Generations

Every time you install or uninstall something, a new generation of a profile is created. You can easily rollback if you did anything wrong

# Roll one generation back
$ nix-env --rollback

# List generations
$ nix-env --list-generations
# Output:
#   1   2023-03-16 23:40:16
#   2   2023-03-16 23:41:01
# ...
#  25   2023-06-01 21:41:55
#  26   2023-06-02 14:38:49   (current)

# Switch generation
$ nix-env -G 23

nix-shell

nix-shell allows you to start a shell session with a specific environment, which can be specified either with the -p and -I cmd flags, or with a .nix file

# Start a shell with Node.JS and curl installed. The specified packages will be 
$ nix-shell -p nodejs curl
# Start a shell with a specific Node.JS version
$ nix-shell -p nodejs -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/34bfa9403e42eece93d1a3740e9d8a02fceafbca.tar.gz
# Specify the shell to run (--command *shell* flag) and do not use the current shell's PATH (--pure flag)
$ nix-shell -p zsh curl --command zsh --pure

Full list of nix-shell arguments

With nix-shell you can also create self-contained reproducible scripts

#!/usr/bin/env nix-shell
#! nix-shell -i bash
#! nix-shell -p jq curl
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz
echo "curl version:"
curl --version
echo "jq version:"
jq --version

If you run such a script and it just shows nothing, it’s probably downloading the nixpkgs revision.

In the nix-shell -i bash line, we set the interpreter for the script with the -i flag. The packages are specified as always. And for the last, the nixpkgs revision is also specified

The Nix language

Time to dive into the Nix language. Take a look at this example to put in a shell.nix file. If you do not understand it, don’t worry, you will likely not have to write something like this in Nix

let
    multipleAndDouble = a: b: a * b * 2;
    curlOriginUrl = "https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz";
    wgetOriginUrl = "https://github.com/NixOS/nixpkgs/archive/7592790b9e02f7f99ddcb1bd33fd44ff8df6a9a7.tar.gz";
    channelFromUrl = url: import (builtins.fetchTarball { url = url; }) {}
in { pkgs ? import <nixpkgs> {}, params ? { "text" = "Some text to show!"; }, a, b }:
pkgs.mkShell rec {
    packageToInclude = if multipleAndDouble a b > 20
        then with channelFromUrl wgetOriginUrl; wget
        else let curlOrigin = channelFromUrl curlOriginUrl;
            in curlOrigin.curl;

    packages = [
        packageToInclude
    ];

    shellHook = let textToShow = params.text; in ''
        echo ${textToShow}
    '';
}

If you are familiar with Haskell, this syntax will look familiar to you. If you don’t understand explanations below, open a Haskell book.

After let we define some local variables for the expression that goes after in. Each binding ends with a semicolon. We define a function named multipleAndDouble by assigning a lambda to this name. In Nix lambdas are defined like this: arg1: arg2: arg3: expr and functions are called like this: func arg1 arg2 arg3. Everything’s curried, so if func takes 3 arguments, func arg1 returns a function that takes the rest of the arguments.

func arg1 arg2 arg3 == (func arg1) arg2 arg3 == ((func arg1) arg2) arg3

Then we define a couple things that’ll be used later.

So the expression we’re putting in our shell.nix file is a lambda. Its argument is a set(object/map) that’s being destructurized. Default values for keys are specified with ?. If we didn’t pass params with --arg params ..., then { "text" = "Some text to show!"; } is used.

In the lambda body, pkgs.mkShell is applied to a recursive set. Recursive set is prefixed with the rec keyword and can refer to its own keys.

packageToInclude’s definition includes an if statement. If the result of applying multipleAndDouble to a and b is greater than 20, then the package we’re gonna fetch is curl and otherwise it’s wget. [Would be] easy to read [if i didn’t use both with and let for demonstration purposes]. with makes set’s keys available at lexic scope after the semicolon.

with channelFromUrl wgetOriginUrl; wget can be written as (channelFromUrl wgetOriginUrl).wget and let curlOrigin = channelFromUrl curlOriginUrl; in curlOrigin.curl can be written as (channelFromUrl curlOriginUrl).curl

Now, to the keys that are used by pkgs.mkShell

packages must be a list of executable packages we want to include in the nix-shell session. By the way, list elements in Nix are separated by spaces.

shellHook is a command that’ll be executed when shell starts. We assign a string with an echo command, and use string interpolation to display textToShow with echo on shell startup.

inputsFrom also exists, it adds all the dependencies from given packages as dependencies.

# Will output "Some text to show!" which is a default value and have wget available because 2*10*2 > 20
$ nix-shell shell.nix --arg a "2" --arg b "10" --pure

# Will output "Hello world!" and have curl available because 2*2*2 < 20
$ nix-shell shell.nix --arg a "2" --arg b "10" --pure --arg params "{ \"text\" = \"Hello world!\"; }"

Note that Nix values are passed in –arg, so if you want to pass a string, it’d be something like --arg strArg '"some text"'

A more common example of a shell.nix file

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
    name = "flask-development-env";
    buildInputs = with pkgs; [
        python39
        python39Packages.flask
        python39Packages.numpy
    ];
    shellHook = ''
        export FLASK_APP=app.py
        export FLASK_ENV=development
    '';
}

Derivations

Derivation - a build task

What we were importing from pkgs are called derivations, not packages and mkShell is just a wrapper around mkDerivation. It’s possible to create an own derivation and pass it to packages

Create a directory hello-world and a src directory inside of it. Put a hello world in C in hello-world/src/main.c and the following in hello-world/default.nix:

with import <nixpkgs> {};
stdenv.mkDerivation {
    # Derivation name
    name = "hello";
    # Package name
    pname = "hello;"
    version = "1.0";

    # The build-time dependencies
    buildInputs = [
        gcc
    ];

    src = ./src;

    # A shell script to build the package
    # If omited, make will be executed
    buildPhase = ''
        gcc -o hello main.c
    '';

    # A shell script to install the package
    installPhase = ''
        mkdir -p $out/bin
        cp hello $out/bin/
    '';

    meta = {
        description = "A hello world in C";
    };
}

It’s also possible to use a build script like this:

# builder.sh
source $stdenv/setup

buildPhase() {
    gcc -o hello main.c
}

installPhase() {
    mkdir -p $out/bin
    cp hello $out/bin/
}

genericBuild

And just put builder = ./builder.sh;

And now, to build the package, run nix-build. default.nix and shell.nix are the only two reserved names. You can run nix-build default.nix too See all the nix-build parameters at the Nix reference manual.

What will happen is addition of that hello world to the Nix store and creation of a link named resul to that directory in Nix store. So the final file structure will look like this

hello-world
├── default.nix
├── result -> /nix/store/lp9nnvp5092li1869vbh1kk5i5yrm9cg-hello
│   └── bin
│       └── hello
└── src
    └── main.c

You can add that hello to your profile by running nix-env -i followed by its path in Nix store. In my case, it’s

$ nix-env -i /nix/store/lp9nnvp5092li1869vbh1kk5i5yrm9cg-hello

mkDerivation’s src can also be pkgs.fetchurl, pkgs.fetchzip, pkgs.fetchgit and even pkgs.fetchFromGitHub. Here’s a list

To use those, you may need to have an SHA256 of something and you can find it by setting the sha256 parameter to pkgs.lib.fakeSha256 and then copying the actual SHA256 from the error

src = pkgs.fetchgit {
  url = "https://gitlab.inria.fr/nix-tutorial/chord-tuto-nix-2022";
  rev = "069d2a5bfa4c4024063c25551d5201aeaf921cb3";
  sha256 = pkgs.lib.fakeSha256;
};

error: hash mismatch in fixed-output derivation '/nix/store/a69pkgkga8zkpys0v5vci7wy6s2faz98-chord-tuto-nix-2022-069d2a5.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-ff4fYS8vJ/Qk8YYpb9kyP/kmJ2xM83f+wrY43NMKijM=
error: 1 dependencies of derivation '/nix/store/lw4rcbn9vlr4g8rc8sx5sfz1dydq3a5r-chord-0.1.0.drv' failed to build

So far those were the essentials of Nix!

Coming soon: flakes

Dm me at @unsafe_andrew for feedback!