Acknowledgment

A few pages are copied from the Scrive Nix Workshop, therefore the following license:

License

Copyright (c) 2024 Tomas Krupka

Copyright (c) 2020-2021 Scrive AB

This work is licensed under both the Creative Commons Attribution-ShareAlike 4.0 International License and the MIT license.

Motivating examples

Specifying dependencies

RUN apt-get update && \
    apt-get install -y \
    libssl1.1 \
    libjson-glib-1.0-0 \
    libgstreamer1.0-0 \
    gstreamer1.0-tools \
    gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-bad \
    gstreamer1.0-plugins-ugly \
    gstreamer1.0-libav \
    libgstrtspserver-1.0-0 \
    libjansson4 \
    make \
    git \
    wget \
    checkinstall

Dependency hell + destructive updates

https://en.wikipedia.org/wiki/Dependency_hell

tomas.krupka@yersinia:~$ pip install awscli

...

Collecting awscli
Collecting s3transfer<0.11.0,>=0.10.0
Collecting botocore==1.34.23

...

Installing collected packages: botocore, s3transfer, awscli
  Attempting uninstall: botocore
    Found existing installation: botocore 1.31.30
    Uninstalling botocore-1.31.30:
      Successfully uninstalled botocore-1.31.30
...

ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
WARNING: boto3 1.28.30 requires botocore<1.32.0,>=1.31.30, but you have botocore 1.34.23 which is incompatible.
WARNING: boto3 1.28.30 requires s3transfer<0.7.0,>=0.6.0, but you have s3transfer 0.10.0 which is incompatible.
Successfully installed awscli-1.32.23 botocore-1.34.23 s3transfer-0.10.0

Multiple software versions and variants

pip install conan==1.59
pip install conan==2.0

apt install opencv
apt install opencv-with-cuda

What happens if an update fails?

  1. The update process itself fails:
apt-get update && apt-get upgrade
...
CTRL+C
  1. The result of the update is undesired
error: invalid magic number
error: you need to load kernel first

Design goals

Nix: A Safe and Policy-Free System for Software Deployment

Existing systems for software deployment are neither safe nor sufficiently flexible. Primary safety issues are the inability to enforce reliable specification of component dependencies, and the lack of support for multiple versions or variants of a component. This renders deployment operations such as upgrading or deleting components dangerous and unpredictable.

The primary features of Nix are:

  • Concurrent installation of multiple versions and variants
  • Atomic upgrades and downgrades
  • Multiple user environments
  • Safe dependencies
  • Complete deployment
  • Transparent binary deployment as an optimisa- tion of source deployment
  • Safe garbage collection
  • Multi-level package management (i.e., centralised + local)
  • Portability

Safe and flexible software deployment

Software deployment is the act of transferring software to the environment where it is to be used. This is a deceivingly hard problem: a number of requirements make effective software deployment difficult in practice, as most current systems fail to be sufficiently safe and flexible.

The main safety issue that a software deployment system must address is consistency: no deployment action should bring the set of installed software components into an inconsistent state. For instance, an installed component should never be able to refer to any component not present in the system; and upgrading or removing components should not break other components or running programs, e.g., by overwriting the files of those components. In particular, it should be possible to have multiple versions and variants of a component installed at the same time.

What is Nix?

Programming Language

  • Dynamically typed - Similar semantics to JavaScript and Lisp.

  • Functional programming - Higher order functions, immutability, etc.

  • Lazy - Values are not evaluated until needed.

Package Manager

  • Packages as special Nix objects that produce derivations and build artifacts.

  • One package can serve as build input of another package.

  • Multiple versions of the "same" package can be present on the same system.

Build System

  • Packages are built from source code.

  • Build artifacts of packages are cached based on content address (SHA256 checksum).

  • Multi language / multi repository build system.

    • Language agnostic.
    • Construct your own build system pipeline.

Operating System

  • Nix itself is a pseudo operating system.

    • Rich set of Nix packages that can typically be found in OS packages.
  • Nix packages can co-exist non-destructively with native OS packages.

    • All Nix artifacts are stored in /nix.

    • Global "installation" is merely a set of symlinks to Nix artifacts in /nix/store.

  • Lightweight activation of global Nix packages.

    • Add ~/.nix-profile/bin/ to $PATH.

    • Call source ~/.nix-profile/etc/profile.d/nix.sh to activate Nix.

    • Otherwise Nix is almost invisible to users if it is not activated.

  • NixOS is a full Linux operating system.

Reproducibility

  • Key differentiation of Nix as compared to other solutions.

  • Nix packages are built inside a lightweight sandbox.

    • No containerization.

    • Sanitize all environment variables.

    • Special $HOME directory at /homeless-shelter.

    • Reset date to Unix time 0.

    • Very difficult to accidentally escape the sandbox.

  • Content-addressable storage.

    • Addresses of Nix packages are based on a checksum of the source code, plus other factors such as CPU architecture and operating system.

    • If the checksum of the source code changes, the addresses of the derivation and any build artifacts also change.

    • If the address of a dependency changes, the addresses of the derivation and build artifact also change.

Installation

Download available at https://nixos.org/download.html.

Simplest way is to run this on Linux:

sh <(curl -L https://nixos.org/nix/install) --daemon

On MacOS:

sh <(curl -L https://nixos.org/nix/install)

After installation, you might need to relogin to your shell to reload the environment. Otherwise, run the following to use Nix immediately:

source ~/.nix-profile/etc/profile.d/nix.sh

Update

If you have installed Nix before but have not updated it for a while, you should update it with:

nix-channel --update

This helps ensure we are installing the latest version of packages in global installation and global imports.

Learning Resources for Nix

  • Official Nix Manuals:

    • Nix Manual - Information about nix-the-command and the Nix Language.
    • Nixpkgs Manual - Information about the Nix Package Set (nixpkgs). How to extend and customise packages, how each language ecosystem is packaged, etc.
    • NixOS Manual - Information about the NixOS operating system. How to install/configure/update NixOS, the DSL for describing configuration options, etc.
  • nix.dev - Pragmatic guide on how to use Nix productively.

  • Awesome Nix - Curated list of Nix resources.

  • Nix Pills - Alternative Nix tutorial. Takes a bottom-up approach, explaining Nix and Nixpkgs design patterns along the way.

  • Nix Whitepaper - Nix: A Safe and Policy-Free System for Software Deployment

External projects

Examples

Nix Shell

You can use Nix packages without installing them globally on your machine. This is a good way to bring in the tools you need for individual projects.

$ nix-shell -p hello

[nix-shell:nix-workshop]$ hello
Hello, world!

Using Multiple Packages

$ nix-shell -p python3 htop vim

[nix-shell:nix-workshop]$ which python3
/nix/store/qp5zys77biz7imbk6yy85q5pdv7qk84j-python3-3.11.6/bin/python3

[nix-shell:nix-workshop]$ which htop
/nix/store/i4af69js47rqcym6j6y83nxj58ihsi96-htop-3.2.2/bin/htop

[nix-shell:nix-workshop]$ which vim
/nix/store/jiixxdyqlaalxzzs0nzhpprgy6rx2bxg-vim-9.0.2048/bin/vim

Using shell.nix

It is common practice for Nix-using projects to provide a shell.nix file that specifies the shell environment. The nix-shell command reads this file, allowing us to create reproducible shell environments without using -p. These environments can provide access to any tool written in any language, without polluting the global environment. We will cover the use of shell.nix in a later chapter.

Nix Dependencies

Check out a package in a Nix Shell:

nix-shell -p subversion graphviz

Runtime dependencies

Print its runtime dependencies (closure):

nix-store --query --requisites $(which svn)

Build dependencies

Print the build-time dependencies of svn:

derivation = $(nix-store --query --valid-derivers $(which svn))

nix-store --query --requisites $derivation

Dependency graph

Plot a graph with Graphviz:

nix-store --query --graph $(which svn) | dot -Tpdf > graph.pdf

Learn more

https://nixos.org/manual/nix/stable/command-ref/nix-store/query#examples

Nix Language Primitives

nix repl
Welcome to Nix 2.18.1. Type :? for help.

nix-repl> "Hello World!"
"Hello World!"

Strings

nix-repl> "hello"
"hello"

Booleans

nix-repl> true
true

nix-repl> false
false

nix-repl> true && false
false

nix-repl> true || false
true

Null

nix-repl> null
null

nix-repl> true && null
error: value is null while a Boolean was expected, at (string):1:1

Numbers

nix-repl> 1
1

nix-repl> 2
2

nix-repl> 1 + 2
3

String Interpolation

nix-repl> name = "John"

nix-repl> name
"John"

nix-repl> "Hello, ${name}!"
"Hello, John!"

Multiline Strings

nix-repl> ''
            Lorem ipsum dolor sit amet, consectetur adipiscing elit.
              Nullam augue ligula, pharetra quis mi porta.

            - ${name}
          ''
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n  Nullam augue ligula, pharetra quis mi porta.\n\n- John\n"

String Concatenation

nix-repl> "Hello " + "World"
"Hello World"

nix-repl> "Hello " + 123
error: cannot coerce an integer to a string, at (string):1:1

Set / Object

nix-repl> object = { foo = "foo val"; bar = "bar val"; }

nix-repl> object
{ bar = "bar val"; foo = "foo val"; }

nix-repl> object.foo
"foo val"

nix-repl> object.bar
"bar val"

Merge Objects

nix-repl> a = { foo = "foo val"; bar = "bar val"; }

nix-repl> b = { foo = "override"; baz = "baz val"; }

nix-repl> a // b
{ bar = "bar val"; baz = "baz val"; foo = "override"; }

Inherit

nix-repl> foo = "foo val"

nix-repl> bar = "bar val"

nix-repl> { foo = foo; bar = bar; }
{ bar = "bar val"; foo = "foo val"; }

nix-repl> { inherit foo bar; }
{ bar = "bar val"; foo = "foo val"; }

List

nix-repl> list = [ "hello" 123 { foo = "foo"; } ]

nix-repl> list
[ "hello" 123 { ... } ]

List Concatenation

nix-repl> [ 1 2 ] ++ [ "foo" "bar" ]
[ 1 2 "foo" "bar" ]

Nix Expressions

If Expression

nix-repl> if true then 0 else 1
0

nix-repl> if false then 0 else "foo"
"foo"

nix-repl> if null then 1 else 2
error: value is null while a Boolean was expected

Let Expression

nix-repl> let
            foo = "foo val";
            bar = "bar val, ${foo}";
          in
          { inherit foo bar; }
{ bar = "bar val, foo val"; foo = "foo val"; }

With Expression

nix-repl> let
            object = {
              foo = "foo val";
              bar = "bar val";
            };
          in
          with object; [
            foo
            bar
          ]
[ "foo val" "bar val" ]

Function

nix-repl> greet = name: "Hello, ${name}!"

nix-repl> greet "Alice"
"Hello, Alice!"

nix-repl> greet "Bob"
"Hello, Bob!"

Curried Function

nix-repl> secret-greet = code: name:
            if code == "secret"
            then "Hello, ${name}!"
            else "Nothing here"

nix-repl> secret-greet "secret" "John"
"Hello, John!"

nix-repl> nothing = secret-greet "wrong"

nix-repl> nothing "Alice"
"Nothing here"

nix-repl> nothing "Bob"
"Nothing here"

Named Arguments

nix-repl> greet = { name, title }: "Hello, ${title} ${name}"

nix-repl> greet { title = "Ms."; name = "Alice"; }
"Hello, Ms. Alice"

nix-repl> greet { name = "Alice"; }
error: anonymous function at (string):1:2 called without required argument 'title', at (string):1:1

Accepting unknown arguments

nix-repl> greet = { name, ... }: "Hello, ${name}"

nix-repl> greet { name = "Alice"; unused = "whatever"; }
"Hello, Alice"

Default Arguments

nix-repl> greet = { name ? "Anonymous", title ? "Ind." }: "Hello, ${title} ${name}"

nix-repl> greet {}
"Hello, Ind. Anonymous"

nix-repl> greet { name = "Bob"; }
"Hello, Ind. Bob"

nix-repl> greet { title = "Mr."; }
"Hello, Mr. Anonymous"

Lazy Evaluation

nix-repl> err = throw "something went wrong"

nix-repl> err
error: something went wrong

nix-repl> if true then 1 else err
1

nix-repl> if false then 1 else err
error: something went wrong

nix-repl> object = { foo = err; bar = "bar val"; }

nix-repl> object.bar
"bar val"

nix-repl> object.foo
error: something went wrong

Library functions

Builtins

These are available by default in the Nix language, reference: Nix manual

String to File

nix-repl> builtins.toFile "hello.txt" "Hello World!"
"/nix/store/r4mvpxzh7rgrm4j831b2yi90zq64grqm-hello.txt"
$ cat /nix/store/r4mvpxzh7rgrm4j831b2yi90zq64grqm-hello.txt
Hello World!

Read File

nix-repl> builtins.readFile ./code/03-nix-basics/03-files/hello.txt
"Hello World!"

nix-repl> builtins.readFile /nix/store/r4mvpxzh7rgrm4j831b2yi90zq64grqm-hello.txt
"Hello World!"

nix-repl> builtins.readFile (builtins.toFile "hello" "Hello World!")
"Hello World!"

Fetch URL

nix-repl> example = builtins.fetchurl "https://scrive.com/robots.txt"

nix-repl> example
[0.0 MiB DL] downloading 'https://scrive.com/robots.txt'"/nix/store/r98i29hkzwyykm984fpr4ldbai2r8lhj-robots.txt"

nix-repl> example
"/nix/store/r98i29hkzwyykm984fpr4ldbai2r8lhj-robots.txt"
$ cat /nix/store/r98i29hkzwyykm984fpr4ldbai2r8lhj-robots.txt
User-agent: *
Sitemap: https://scrive.com/sitemap.xml
Disallow: /amnesia/
Disallow: /api/

URLs are only fetched once locally!

Fetch Tarball

nix-repl> nodejs-src = builtins.fetchTarball
            "https://nodejs.org/dist/v14.15.0/node-v14.15.0-linux-x64.tar.xz"
nix-repl> nodejs-src
"/nix/store/6wkj0blipzdqbsvwv03qy57n4l33scpw-source"
$ ls /nix/store/6wkj0blipzdqbsvwv03qy57n4l33scpw-source
bin  CHANGELOG.md  include  lib  LICENSE  README.md  share

Nixpkgs library functions

These have to be imported from the nixpkgs repository, reference: Nixpkgs manual

nix-repl> :l <nixpkgs>

nix-repl> object = { a = 1; b = 2; c = true; }

nix-repl> lib.attrNames object
[ "a" "b" "c" ]

nix-repl> lib.attrValues object
[ 1 2 true ]

Finding the main executable of a program:

nix-repl> lib.getExe mpv      
"/nix/store/rw37dfsb0sijl5ld7ihc17295xdr66q5-mpv-with-scripts-0.36.0/bin/mpv"

Custom Nix Shell

Python

Example is forked from Setting up a Python development environment, having the following app myapp.py:

#!/usr/bin/env python

import random
import subprocess
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    result = subprocess.run(
        ["cowsay", random.choice(["Nix rules!"] * 6 + ["Nix sucks!"])],
        capture_output=True,
    )
    result.check_returncode()
    return {"message": result.stdout.decode()}

def run():
    app.run(host="0.0.0.0", port=5000)

if __name__ == "__main__":
    run()

Let's create an evironment with app dependencies + extra developer tools, named myapp.nix:

{ pkgs ? import <nixpkgs> { } }:

pkgs.mkShell {
  packages = with pkgs; [
    (python3.withPackages (ps: [ ps.flask ]))
    curl
    jq
    cowsay
  ];
}

Enter the shell and run the app:

nix-shell myapp.nix
python ./myapp.py &
curl 127.0.0.1:5000 | jq -r '.message'

C++

Let's run the Boost Python hello world, filename boost.cc:

#include <boost/python.hpp>

char const* greet()
{
   return "hello, world";
}

BOOST_PYTHON_MODULE(hello_ext)
{
    boost::python::def("greet", greet);
}

Standard CMakeLists.txt file:

cmake_minimum_required(VERSION 3.5)
project(BoostPythonHello)

find_package(Python 3 REQUIRED COMPONENTS Development)
find_package(Boost COMPONENTS python REQUIRED)
set(CMAKE_SHARED_MODULE_PREFIX "")

add_library(hello_ext MODULE boost.cc)
target_link_libraries(hello_ext PRIVATE Boost::python Python::Module)

Nix environment boost.nix, notice that we override the default boost package options to get python support:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  packages = with pkgs; 
    let 
      myPython = python3.withPackages (ps: [ ps.matplotlib ps.numpy ]);
    in
  [
    (boost.override {
      enablePython = true;
      enableNumpy = true;
      python = myPython;
    })
    myPython
    cmake
  ];
}

Build and run:

nix-shell boost.nix

cmake -S . -B build
cmake --build build
cd build

python3
>>> import hello_ext
>>> hello_ext.greet()
'hello, world'

Custom Python package 1

Let's package the cowsay server from before, so that others can use it in their environments. This is done using the stdenv.mkDerivation, a package is a function defined in a .nix file taking all the build inputs and outputting its results in a special $out location.

First try:

{ stdenv
, python3
}:

stdenv.mkDerivation rec {
  pname = "cowsayer";
  version = "0.0.1";

  src = [
    ./myapp.py
  ];

  dontUnpack = true;

  # can be used at build time only
  nativeBuildInputs = [ ];

  # can be used at build time and run time
  buildInputs = [
    (python3.withPackages (ps: [ ps.flask ]))
  ];

  buildPhase = ''
    sleep 1
  '';

  installPhase = ''
    install -m755 -D ${./myapp.py} $out/bin/cowsayer-server
  '';
}

Let's try building it:

nix-build myapp_v1.nix

Better:

nix-build -E 'with import <nixpkgs> {}; callPackage ./myapp_v1.nix {}'

Test:

./result/bin/cowsayer-server
nix-shell -p curl jq
curl 127.0.0.1:5000 | jq -r '.message'

Custom Python package 2

Second try:

{ stdenv
, python3
, cowsay
, makeWrapper
, lib
}:

stdenv.mkDerivation rec {
  pname = "cowsayer";
  version = "0.0.2";

  src = [
    ./myapp.py
  ];

  dontUnpack = true;

  # can be used at build time only
  nativeBuildInputs = [
    makeWrapper
  ];

  # can be used at build time and run time
  buildInputs = [
    (python3.withPackages (ps: [ ps.flask ]))
  ];

  buildPhase = ''
    sleep 1
  '';

  installPhase = ''
    install -m755 -D ${./myapp.py} $out/bin/cowsayer-server
    wrapProgram $out/bin/cowsayer-server \
      --prefix PATH : ${lib.makeBinPath [ cowsay ]}
  '';
}

Build:

nix-build -E 'with import <nixpkgs> {}; callPackage ./myapp_v2.nix {}'

Test:

./result/bin/cowsayer-server
nix-shell -p curl jq
curl 127.0.0.1:5000 | jq -r '.message'

Custom Python package 3

Second try:

{ stdenv
, python3
, cowsay
, makeWrapper
, lib
, writeShellApplication
, curl
, jq
, verbose ? false
}:

stdenv.mkDerivation rec {
  pname = "cowsayer";
  version = "0.0.2";

  src = [
    ./myapp.py
  ];

  dontUnpack = true;

  # can be used at build time only
  nativeBuildInputs = [
    makeWrapper
  ];

  # can be used at build time and run time
  buildInputs = [
    (python3.withPackages (ps: [ ps.flask ]))
  ];

  buildPhase = ''
    sleep 1
  '';

  installPhase =
    let
      client = writeShellApplication {
        name = "nixcow";
        runtimeInputs = [ curl jq ];
        text = ''
          curl 127.0.0.1:5000 ${lib.optionalString (!verbose) "-s"} | jq -r '.message'
        '';
      };
    in
    ''
      install -m755 -D ${./myapp.py} $out/bin/cowsayer-server
      wrapProgram $out/bin/cowsayer-server \
        --prefix PATH : ${lib.makeBinPath [ cowsay ]}

      ln -s ${lib.getExe client} $out/bin/nixcow
    '';
}

Build:

nix-build -E 'with import <nixpkgs> {}; callPackage ./myapp_v3.nix { verbose = true; }'

Test:

./result/bin/cowsayer-server
./result/bin/nixcow

C++ package

Let's package the following app: https://github.com/krupkat/microbench_test in a bench.nix file

{ stdenv
, cmake
, boost
, gbenchmark
, fetchurl
}:

stdenv.mkDerivation rec {
  pname = "microbench_test";
  version = "0.0.2";

  src = fetchurl {
    url = "https://github.com/krupkat/${pname}/archive/refs/tags/${version}.tar.gz";
    sha256 = "0gzzi3bg4yrlcmimjib2hz6r5djk5v3y7a0azcpypch90a9f2k1i";
  };

  # can be used at build time only
  nativeBuildInputs = [ 
    cmake
  ];

   # can be used at build time and run time
  buildInputs = [
    boost
    gbenchmark
  ];
}

Build and run:

nix-build -E 'with import <nixpkgs> {}; callPackage ./bench.nix {}'
./result/bin/bench

note on the Nix language, why?

Package overriding 1

A powerful feature if Nix is the option to override any packages inputs (docs).

Let's use this to compare multiple builds of our benchmark application. Create the following multibench_v1.nix shell file:

{ pkgs ? import <nixpkgs> { }
, lib ? pkgs.lib
}:

let
  args = lib.cartesianProductOfSets {
    cxxstd = [ "11" "20" ];
    stdenv = [ pkgs.gcc13Stdenv pkgs.llvmPackages_17.stdenv ];
  };

  overrideBoost = { cxxstd, stdenv }: {
    name = "std${cxxstd}-${stdenv.cc.name}";
    stdenv = stdenv;
    boost = pkgs.boost.override {
      extraB2Args = [ "cxxstd=${cxxstd}" ];
      stdenv = stdenv;
    };
  };

  boostList = map overrideBoost args;

  customizeBench = { name, stdenv, boost }:
    pkgs.writeShellApplication {
      name = "bench-${name}";
      runtimeInputs = [
        (pkgs.callPackage ./bench.nix {
          inherit stdenv boost;
        })
      ];
      text = "bench";
    };
in

pkgs.mkShell rec {
  packages = map customizeBench boostList;

  shellHook =
    let
      runTest = test: ''
        echo "Running ${test.name}:"
        ${test.name}
      '';
    in
    lib.concatMapStringsSep "\n" runTest packages;
}

And test it:

nix-shell multibench_v1.nix

bench-std11-clang-wrapper-17.0.6
bench-std20-gcc-wrapper-13.2.0

Package overriding 2

On top of overriding package inputs, we can also override any of its attributes. Those can be either the existing ones (like src) or we can add new ones, like one of the many hooks (preBuild, postInstall, ... check the list of build phases).

We use this modify the source code to run the benchmark on a larger hashmap.

There is also additional shellHook that auto runs all the benchmarks when entering the shell. The file is named multibench_v2.nix:

{ pkgs ? import <nixpkgs> { }
, lib ? pkgs.lib
}:

let
  args = lib.cartesianProductOfSets {
    cxxstd = [ "11" "20" ];
    stdenv = [ pkgs.gcc13Stdenv pkgs.llvmPackages_17.stdenv ];
  };

  overrideBoost = { cxxstd, stdenv }: {
    name = "std${cxxstd}-${stdenv.cc.name}";
    stdenv = stdenv;
    boost = pkgs.boost.override {
      extraB2Args = [ "cxxstd=${cxxstd}" ];
      stdenv = stdenv;
    };
  };

  boostList = map overrideBoost args;

  customizeBench = { name, stdenv, boost }:
    pkgs.writeShellApplication {
      name = "bench-${name}";
      runtimeInputs =
        let
          test = pkgs.callPackage ./bench.nix {
            inherit stdenv boost;
          };
        in
        [
          (test.overrideAttrs
            (final: prev: {
              preConfigure = ''
                substituteInPlace benchmark.cc --replace "10000" "1000000"
              '';
            }))
        ];
      text = "bench";
    };
in

pkgs.mkShell rec {
  packages = map customizeBench boostList;

  shellHook =
    let
      runTest = test: ''
        echo "Running ${test.name}:"
        ${test.name}
      '';
    in
    lib.concatMapStringsSep "\n" runTest packages;
}

Build and run:

nix-shell multibench_v2.nix

Nix channels and pinning

Where do the packages come from?

nix repl

nix-repl> :l <nixpkgs>
opencv
echo $NIX_PATH

List channels:

nix-channel --list

nixos https://nixos.org/channels/nixos-23.11
nixos-unstable https://nixos.org/channels/nixos-unstable
sops-nix https://github.com/Mic92/sops-nix/archive/master.tar.gz

Load a specific channel:

nix-repl> :l <nixos-unstable>

Mix channels in your environment:

{ pkgs ? import <nixos> { }
, pkgs-unstable ? import <nixos-unstable> {}
}:

pkgs.mkShell {
  packages = [
    pkgs.vlc
    pkgs-unstable.vlc
  ];
}

Pin your dependencies:

https://nix.dev/tutorials/first-steps/towards-reproducibility-pinning-nixpkgs.html

{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/5f5210aa20e343b7e35f40c033000db0ef80d7b9.tar.gz") {}
}:

pkgs.mkShell {
  packages = [
    pkgs.vlc
  ];
}

Picking the commit can be done via status.nixos.org, which lists all the releases and the latest commit that has passed all tests.

NixOS

Taking the ideas of the Nix package manager and applying them to the whole operating system.

The whole system is defined in a configuration file, usually placed in /etc/nixos/configuration.nix.

This is a minimal configuration.nix example:

{ config, pkgs, ... }:

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

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  services.xserver.enable = true;
  services.xserver.displayManager.gdm.enable = true;
  services.xserver.desktopManager.gnome.enable = true;

  users.users.alice = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
    packages = with pkgs; [
      firefox
      vlc
    ];
  };

  system.stateVersion = "23.11";
}

With hardware-configuration.nix being specific to a computer and usually autogenerated by nixos-generate-config:

{ config, lib, pkgs, modulesPath, ... }:

{
  boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" ];
  boot.kernelModules = [ "kvm-amd" ];

  fileSystems."/" =
    { device = "/dev/disk/by-uuid/6132cd3c-aba6-4f51-9360-a064e98f6942";
      fsType = "ext4";
    };

  boot.initrd.luks.devices."luks-fde45edd-d98c-4e9e-aa8a-541266584cc0".device = "/dev/disk/by-uuid/fde45edd-d98c-4e9e-aa8a-541266584cc0";

  fileSystems."/boot" =
    { device = "/dev/disk/by-uuid/FADD-B2BE";
      fsType = "vfat";
    };

  networking.useDHCP = lib.mkDefault true;
  nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
  hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

Options

All available options are searchable on https://search.nixos.org/options, they are implemented in the nixpkgs repository (https://github.com/NixOS/nixpkgs/tree/master/nixos).

Rebuilding

Build the system and switch to the new configuration:

sudo nixos-rebuild switch

no manual sudo, all changes saved in git

NixOS + overriding

Let's customize one of the installed packages in the system (vlc);

{ config, pkgs, callPackage, ... }:

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

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  services.xserver.enable = true;
  services.xserver.displayManager.gdm.enable = true;
  services.xserver.desktopManager.gnome.enable = true;

  users.users.alice = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
    packages = with pkgs; 
      let 
        customFFMpeg = callPackage ./build_my_ffmpeg.nix {};
    [
      firefox
      (vlc.override { ffmpeg_4 = customFFMpeg; })
    ];
  };

  system.stateVersion = "23.11";
}

Benefit: custom system components + up to date versions.

Custom modules

Separate functionality in custom modules and reuse where needed by importing + passing options.

zswap module

{ config, lib, pkgs, ... }:

let cfg = config.krupkat.system.zswap;

in {
  options.krupkat.system.zswap.enable = lib.mkEnableOption "Zswap";

  config = lib.mkIf cfg.enable {
    boot = {
      initrd = {
        kernelModules = [ "lz4" "z3fold" ];
        preDeviceCommands = ''
          printf lz4 > /sys/module/zswap/parameters/compressor
          printf z3fold > /sys/module/zswap/parameters/zpool
          printf 25 > /sys/module/zswap/parameters/max_pool_percent
        '';
      };

      kernel.sysctl = {
        "vm.swappiness" = 180;
        "vm.page-cluster" = 0;
      };
      kernelParams = [ "zswap.enabled=1" ];
    };
  };
}

Import and enable in the main configuration.nix file:

{ config, lib, pkgs, ... }:

{
  imports =
    [
      ./modules/zswap.nix
    ];

  krupkat.system.zswap.enable = true;

  ...
}

Home manager

Taking the ideas of the Nix package manager and applying them to a users home folder (Home manager, Home manager options).

{ config, lib, pkgs, ... }:

{
  home.username = "tom";
  home.homeDirectory = "/home/tom";

  home.stateVersion = "23.05";

  home.packages = with pkgs; [
    atool
    cinnamon.nemo
    clinfo
    dstat
    gimp
    gnome.file-roller
    gnome.gedit
    gnome.gnome-system-monitor
    gnome.simple-scan
    lm_sensors
    firefox
    gthumb
    htop
    libarchive
    libreoffice
    mpv
    nixpkgs-fmt
    nix-index
    nix-prefetch-github
    radeontop
    rescale
    unzip
    vlc
    wget
    xpano
  ];

  home.file = {
    ".config/dunst".source = ./dunst;
    ".config/hypr/hyprland.conf".source =
      pkgs.substituteAll { src = ./hyprland/template.conf; launch_waybar = ./hyprland/waybar.sh; };
    ".config/hypr/hyprpaper.conf".source =
      pkgs.substituteAll { src = ./hyprpaper/template.conf; wallpaper = ./hyprpaper/nixos.png; };
    ".config/kitty".source = ./kitty;
    ".config/rofi".source = ./rofi;
    ".config/swayidle".source = ./swayidle;
    ".config/swaylock".source = ./swaylock;
    ".config/waybar".source = ./waybar;
    ".config/wireplumber".source = ./wireplumber;
    ".local/share/gedit".source = ./gedit;
  };

  programs.home-manager.enable = true;

  programs.vscode = {
    enable = true;
    package = pkgs.vscodium;
    extensions = with pkgs; [
      vscode-extensions.dracula-theme.theme-dracula
      vscode-extensions.jnoortheen.nix-ide
      vscode-extensions.llvm-vs-code-extensions.vscode-clangd
      vscode-extensions.ms-vscode.cmake-tools
      vscode-extensions.twxs.cmake
      vscode-extensions.xaver.clang-format
      vscode-extensions.vadimcn.vscode-lldb
    ];
  };

  programs.git = {
    enable = true;
    userName = "Tomas Krupka";
    userEmail = "6817216+krupkat@users.noreply.github.com";
  };

  programs.bash.enable = true;

  programs.vim = {
    enable = true;
    extraConfig = ''
      set nu
      syntax enable
      let g:dracula_colorterm = 0
      colorscheme dracula
    '';
    plugins = [ pkgs.vimPlugins.dracula-vim ];
  };

  gtk = {
    enable = true;
    theme = {
      name = "Dracula";
      package = pkgs.dracula-theme;
    };
    iconTheme = {
      name = "Dracula";
      package = dracula-icon-theme-custom;
    };
  };
}

NixOS examples

With the power of the NixOS options, you can customize all the parts of your system without having to switch between contexts / different configuration paradigms / languages.

You can share variables in the Nix configuration across the different components

  • e.g. when configuring server, the domain name and specific service ports can be defined at the top of the configuration file and then reused everywhere
  • this removes duplication in your system configuration

Boot opions

Latest kernel + kernel parameter + systemd-boot instead of grub

{
  boot.kernelPackages = pkgs.linuxPackages_latest;
  boot.kernelParams = [ "amd_pstate=active" ];
  boot.loader.systemd-boot.enable = true;
}

System packages

Packages available to all system users:

{
  environment.systemPackages = with pkgs; [
    gnome.seahorse
    sshfs
  ]
}

User management

{
  users.users.tom = {
    isNormalUser = true;
    description = "T.K.";
    extraGroups = [ "networkmanager" "wheel" "video" ];
  };

  users.users.nixremote = {
    isNormalUser = true;
     openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFa5xTjWp9+btqQ0hkJiU3gys0xD3/uCXK48ZbzlMvjL"
    ];
  };
}

Install hardware drivers

{
  hardware.bluetooth.enable = true;
  hardware.opengl.enable = true;
}

Installing fonts

{
  fonts.packages = with pkgs; [
    font-awesome
    nerdfonts
    cantarell-fonts
    iosevka
  ];
}

Networking

{
  networking = {
    hostName = "ideapad";
    networkmanager.enable = true;
    networkmanager.dns = "none";
    nameservers = [ "127.0.0.1" "::1" ];
  };
}

Enable services

SSH

{
  services.openssh = {
    enable = true;
    settings.PasswordAuthentication = false;
  };
}

Builtin services

Nix Options

{
  services.fstrim.enable = true;
  services.printing.enable = true;
  services.gnome.gnome-keyring.enable = true;
  services.blueman.enable = true;
  services.dnscrypt-proxy2.enable = true;
}

Custom systemd service

{
  systemd.services.suspend = {
    description = "User suspend actions";
    before = [ "sleep.target" ];
    wantedBy = [ "sleep.target" ];
    serviceConfig = {
      ExecStart = "systemd-run --user --machine=tom@ ${pkgs.swaylock}/bin/swaylock";
      ExecStartPost = "${pkgs.coreutils}/bin/sleep 1";
    };
  };
}

Custom systemd timer

{
  systemd.timers.inadyn = {
    description = "Sync ddns every ${cfg.period}";
    wantedBy = [ "default.target" ];
    timerConfig = {
      OnBootSec = "2m";
      OnUnitActiveSec = cfg.period;
    };
  };
}

Experiment without fear!

  • different kernels + kernel arguments
  • different gpu drivers
  • different OpenCL + Vulkan implementations
  • kernel modules (zram + zswap)
  • power management
  • custom cpu microcode updates
  • custom dns server
  • get rid of x11
  • ...

when you're done... you're done!

Remote builds

NixOS rebuild can be run to build locally and push to a target (docs):

nixos-rebuild --target-host tomaskrupka.cz --use-remote-sudo switch -I nixos-config=configuration.nix

Or to build remotely and deploy locally with --build-host.

This works across architectures.

Distributed builds

We can setup the nix.buildMachines option wit ha list of remote builders, e.g.:

{
  nix.buildMachines = [ {
      hostName = "builder";
      system = "x86_64-linux";
      maxJobs = 12;
      speedFactor = 2;
      supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
    }
  ...
  ] ;
}

The rebuild process then transparently chooses the best target to build on based on architecture / priority.

Cross compilation

Copied from the tutorial: https://nix.dev/tutorials/cross-compilation.html

Available build environments

nix repl '<nixpkgs>'
Welcome to Nix version 2.3.12. Type :? for help.

Loading '<nixpkgs>'...
Added 14200 variables.

nix-repl> pkgsCross.<TAB>
pkgsCross.aarch64-android             pkgsCross.musl-power
pkgsCross.aarch64-android-prebuilt    pkgsCross.musl32
pkgsCross.aarch64-darwin              pkgsCross.musl64
pkgsCross.aarch64-embedded            pkgsCross.muslpi
pkgsCross.aarch64-multiplatform       pkgsCross.or1k
pkgsCross.aarch64-multiplatform-musl  pkgsCross.pogoplug4
pkgsCross.aarch64be-embedded          pkgsCross.powernv
pkgsCross.amd64-netbsd                pkgsCross.ppc-embedded
pkgsCross.arm-embedded                pkgsCross.ppc64
pkgsCross.armhf-embedded              pkgsCross.ppc64-musl
pkgsCross.armv7a-android-prebuilt     pkgsCross.ppcle-embedded
pkgsCross.armv7l-hf-multiplatform     pkgsCross.raspberryPi
pkgsCross.avr                         pkgsCross.remarkable1
pkgsCross.ben-nanonote                pkgsCross.remarkable2
pkgsCross.fuloongminipc               pkgsCross.riscv32
pkgsCross.ghcjs                       pkgsCross.riscv32-embedded
pkgsCross.gnu32                       pkgsCross.riscv64
pkgsCross.gnu64                       pkgsCross.riscv64-embedded
pkgsCross.i686-embedded               pkgsCross.scaleway-c1
pkgsCross.iphone32                    pkgsCross.sheevaplug
pkgsCross.iphone32-simulator          pkgsCross.vc4
pkgsCross.iphone64                    pkgsCross.wasi32
pkgsCross.iphone64-simulator          pkgsCross.x86_64-embedded
pkgsCross.mingw32                     pkgsCross.x86_64-netbsd
pkgsCross.mingwW64                    pkgsCross.x86_64-netbsd-llvm
pkgsCross.mmix                        pkgsCross.x86_64-unknown-redox
pkgsCross.msp430

nix-repl> pkgsCross.aarch64-multiplatform.stdenv.hostPlatform.config
"aarch64-unknown-linux-gnu"

Simple example

let
  pkgs = import <nixpkgs> { crossSystem = { config = "aarch64-unknown-linux-gnu"; }; };
in

pkgs.callPackage ../04_benchmark/bench.nix {}

Run:

nix-build simple.nix

./result/bin/bench

Advanced example

let
  pkgs = import <nixpkgs> {};

  # Create a C program that prints Hello World
  helloWorld = pkgs.writeText "hello.c" ''
    #include <stdio.h>

    int main (void)
    {
      printf ("Hello, world!\n");
      return 0;
    }
  '';

  # A function that takes host platform packages
  crossCompileFor = hostPkgs:
    # Run a simple command with the compiler available
    hostPkgs.runCommandCC "hello-world-cross-test" {} ''
      # Wine requires home directory
      HOME=$PWD

      # Compile our example using the compiler specific to our host platform
      $CC ${helloWorld} -o hello

      # Run the compiled program using user mode emulation (Qemu/Wine)
      # buildPackages is passed so that emulation is built for the build platform
      ${hostPkgs.stdenv.hostPlatform.emulator hostPkgs.buildPackages} hello > $out

      # print to stdout
      cat $out
    '';
in {
  rpi = crossCompileFor pkgs.pkgsCross.raspberryPi;
  windows = crossCompileFor pkgs.pkgsCross.mingwW64;
}

Run:

nix-build advanced.nix

cat result
cat result-2

Non-trivial example

VM running on NixOS: https://github.com/krupkat/gcp-nixos.

Hosted at tomaskrupka.cz