Convenient Management of Inputs with Nix Flakes

In the previous sections we showed Nix derivations pinning a specific commit of nixpkgs. To ensure the reproducibility of the package, we also verify that the sha256 of the fetched tarball is the one expected. This approach is however quite cumbersome to maintain when managing several inputs.

Activating Nix Flakes

At the time of this tutorial (July 2022), Nix Flakes are still an experimental feature and require some extra configuration to activate.

echo 'experimental-features = nix-command flakes' >> /etc/nix/nix.conf

or

echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf

You should now be able to use the nix command.

nix flake --version

Creating a new flake

Nix Flake provide a convenient template system that makes starting new project much easier. There exist default templates, and users can also define their own.

nix flake show templates
github:NixOS/templates/2f86534428917d96d414964c69a5cfe353500ad5
├───defaultTemplate: template: A very basic flake
└───templates
    ├───bash-hello: template: An over-engineered Hello World in bash
    ├───c-hello: template: An over-engineered Hello World in C
    ├───compat: template: A default.nix and shell.nix for backward compatibility with Nix installations that don't support flakes
    ├───full: template: A template that shows all standard flake outputs
    ├───go-hello: template: A simple Go package
    ├───haskell-hello: template: A Hello World in Haskell with one dependency
    ├───hercules-ci: template: An example for Hercules-CI, containing only the necessary attributes for adding to your project.
    ├───pandoc-xelatex: template: A report built with Pandoc, XeLaTex and a custom font
    ├───python: template: Python template, using poetry2nix
    ├───rust: template: Rust template, using Naersk
    ├───rust-web-server: template: A Rust web server including a NixOS module
    ├───simpleContainer: template: A NixOS container running apache-httpd
    └───trivial: template: A very basic flake

Based on the nature of the project, users can pick a template to kickstart their project.

In our case, we will use the trivial template.

To import a template, we can use the following commands:

nix flake init -t templates#trivial

The nix flake init command will instantiate the template in the current directory. To create a new directory with the content of the template instead, we can use the nix flake new -t templates#trivial myfolder. The -t flag indicates to look for templates in the templates flake, and in particular the trivial template.

But Nix Flakes can have default values, like templates. In this case, the default template (defaultTemplate) is the trivial template. Thus, we can actually just call:

nix flake init -t templates

Actually, the flake templates is the default one for templates. So you could even get the trivial template by running:

nix flake init

Nix Flakes require to be in a Git repository to work

git init && git add flake.nix

You can now try to show the content of the flake:

nix flake show
git+file:///tmp/tuto-nix
├───defaultPackage
│   └───x86_64-linux: package 'hello-2.12'
└───packages
    └───x86_64-linux
            └───hello: package 'hello-2.12'

This flake contains one package available for the x86_64-linux targets: the hello package.

To build a package, run:

nix build .#hello

This tells nix to build the hello package from the flake in the current directory ..

It will create a result folder linking to the result of the derivation in the store.

Execute the built hello package:

./result/bin/hello
Hello, world!

As the produced package has a binary with the same name of the derivation (hello here) we can even use the nix run command to execute the hello package:

nix run .#hello
Hello, world!

It is now time to open this mysterious flake.nix file.

{
  description = "A very basic flake";

  outputs = { self, nixpkgs }: {

        packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;

        defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;

  };
}

A flake.nix file has 3 attributes:

  • description a string for commenting the flake
  • inputs a set defining the dependencies of the flake
  • outputs a function, taking the dependencies as inputs, and returning a set of the outputs

In the example above, the flake.nix file does not have a inputs field and will thus rely on the nixpkgs registry of the host machine (see below). This is not as dirty as using channels because the version of nixpkgs used is store in the flake.lock file, thus ensuring tracability and reproducibility.

Declaring Inputs

Let us add an input to the flake. We will add a specific version of nixpkgs.

{
  description = "A very basic flake";

  inputs = {
        nixpkgs.url = "github:nixos/nixpkgs/22.05";
  };

  outputs = { self, nixpkgs }: {

        packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;

        defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;

  };
}

Here, we tell the flake to use the version of nixpkgs from the github repository with the tag 22.05.

Now, the content of the flake.lock is no more coherent with the desired version of nixpkgs. Nix will realise this at the next evaluation of the flake and update the lock file:

nix flake show
warning: updating lock file '/tmp/tuto-nix/flake.lock':
• Updated input 'nixpkgs':
        'github:NixOS/nixpkgs/95e79164be1f7d883ed9ffda8b7d4ad3a17e6c1e' (2022-07-01)
        'github:nixos/nixpkgs/ce6aa13369b667ac2542593170993504932eb836' (2022-05-30)
git+file:///tmp/tuto-nix
├───defaultPackage
│   └───x86_64-linux: package 'hello-2.12'
└───packages
        └───x86_64-linux
                └───hello: package 'hello-2.12'

In the flake.lock file, we can now see the updated lock of the nixpkgs input:

{
  "nodes": {
        "nixpkgs": {
          "locked": {
                "lastModified": 1653936696,
                "narHash": "sha256-M6bJShji9AIDZ7Kh7CPwPBPb/T7RiVev2PAcOi4fxDQ=",
                "owner": "nixos",
                "repo": "nixpkgs",
                "rev": "ce6aa13369b667ac2542593170993504932eb836",
                "type": "github"
          },
          "original": {
                "owner": "nixos",
                "ref": "22.05",
                "repo": "nixpkgs",
                "type": "github"
          }
        },
        "root": {
          "inputs": {
                "nixpkgs": "nixpkgs"
          }
        }
  },
  "root": "root",
  "version": 7
}

The commit of nixpkgs is ce6aa13 which is the one associated to the 22.05 tag (https://github.com/NixOS/nixpkgs/tree/22.05).

Updating Inputs

To update the version of all the inputs, run nix flake update. In the case where you only want to update a single input, use nix flake lock --update-input <INPUT_NAME>. For the nixpkgs input, this will be nix flake lock --update-input nixpkgs.

Defining Outputs

A difference between the classical Nix expressions and the Nix Flake is the way the outputs are organized. The outputs field is a function taking as input the inputs and returning a set. This set should have a specific hierarchy. First the type of output (packages, devShells, checks,…), then the target architecture (x86_64-linux, aarch64-linux, x86_64-darwin,…) and finally the name of the output. In the example in the template above, you can see that it defines a package for x86_64-linux architecture named hello. Note that some type of outputs, like templates or overlays, do not require a target architecture as they are common to all of them.

Let us add our packages defining previously. In the repository of your packages repository, start a Nix Flake:

{
  description = "A very basic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/22.05";
  };

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      packages.${system} = rec {
        chord = pkgs.callPackage ./pkgs/chord {};
        chord_custom_sg = pkgs.callPackage ./pkgs/chord { simgrid = custom_simgrid; };
        custom_simgrid = pkgs.callPackage ./pkgs/simgrid/custom.nix {};
      };
    };
}

Backwards Compatibility

As Nix Flakes are still an experimental feature (at the time of this tutorial), you may want to provide a way for the users to interact with a flake via the classical nix CLI (i.e., nix-build, nix-shell, …).

In the folder containing your flake.nix and flake.lock, add the following in a file named default.nix.

(import (
  fetchTarball {
        url = "https://github.com/edolstra/flake-compat/archive/12c64ca55c1014cdc1b16ed5a804aa8576601ff2.tar.gz";
        sha256 = "0jm6nzb83wa6ai17ly9fzpqc40wg1viib8klq8lby54agpl213w5"; }
) {
  src =  ./.;
}).defaultNix

You can now interact with the Nix Flake through the classical Nix CLI. Note however that the name of the target attribute will be the full output name. In the example above, nix-build -A hello will not work as there is no output named hello, you should instead use nix-build -A packages.x86_64-linux.hello.

Registries

Similar to channels presented previously, Nix Flakes have what is called registry. You can see the list of available registries with

nix registry list
global flake:agda github:agda/agda
global flake:blender-bin github:edolstra/nix-warez?dir=blender
global flake:dreampkgs github:nix-community/dreampkgs
global flake:dwarffs github:edolstra/dwarffs
global flake:emacs-overlay github:nix-community/emacs-overlay
global flake:fenix github:nix-community/fenix
global flake:flake-utils github:numtide/flake-utils
global flake:gemini github:nix-community/flake-gemini
global flake:home-manager github:nix-community/home-manager
global flake:hydra github:NixOS/hydra
global flake:mach-nix github:DavHau/mach-nix
global flake:nimble github:nix-community/flake-nimble
global flake:nix github:NixOS/nix
global flake:nix-darwin github:LnL7/nix-darwin
global flake:nixops github:NixOS/nixops
global flake:nixos-hardware github:NixOS/nixos-hardware
global flake:nixos-homepage github:NixOS/nixos-homepage
global flake:nixos-search github:NixOS/nixos-search
global flake:nur github:nix-community/NUR
global flake:nixpkgs github:NixOS/nixpkgs/nixpkgs-unstable
global flake:templates github:NixOS/templates
global flake:patchelf github:NixOS/patchelf
global flake:poetry2nix github:nix-community/poetry2nix
global flake:nix-serve github:edolstra/nix-serve
global flake:nickel github:tweag/nickel
global flake:bundlers github:NixOS/bundlers

You can inspect any of those flake by simply referencing their name, for example the flake-utils flake:

nix flake show flake-utils
github:numtide/flake-utils/7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249
├───defaultTemplate: template: A flake using flake-utils.lib.eachDefaultSystem
├───lib: unknown
└───templates
        ├───check-utils: template: A flake with tests
        ├───each-system: template: A flake using flake-utils.lib.eachDefaultSystem
        └───simple-flake: template: A flake using flake-utils.lib.simpleFlake

You can add user-defined registries, which can be really helpful for end users. For example, we have defined a flake for our own packages repository (as seen previously). Here is an example: https://gitlab.inria.fr/qguillot/mypkgs_example.

nix registry add mypkgs git+https://gitlab.inria.fr/qguillot/mypkgs_example

The previous command adds our flake defining our own packages to the list of registries. It can now be accessed by the name mypkgs.

nix flake show mypkgs
git+https://gitlab.inria.fr/qguillot/mypkgs_example?ref=master&rev=1ca007939e4dbcc9ca80830301ea1e120c3fac2d
├───packages
│   └───x86_64-linux
│       ├───chord: package 'chord-0.1.0'
│       └───chord-docker: package 'docker-image-chord-docker.tar.gz'
└───templates
        ├───article: template: A template for an article repository
        ├───default: template: A template to use as a flake starting point
        └───flake: template: A template to use as a flake starting point

You can now enter a shell with chord via the command:

nix shell mypkgs#chord

Use our flake for experiments

To use the packages, or other outputs, in another flake, we just need to add it to the inputs set. Let us create a flake for an experiments repository that will create a shell with the chord package available.

{
  description = "My Experiments repo";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/22.05";
    mypkgs.url = "git+https://gitlab.inria.fr/qguillot/mypkgs_example";
  };

  outputs = { self, nixpkgs, mypkgs }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in {
      devShells.${system} = {
        chordShell = pkgs.mkShell {
          buildInputs = [
            mypkgs.packages.${system}.chord
          ];
        };
    };
  };
}

Don’t forget to add mypkgs as a parameter of the outputs function. To access a package of our flake, we use its full name (i.e., type of output + system + name). In the case of chord, it is mypkgs.packages.${system}.chord.

nix flake show
└───devShells
        └───x86_64-linux
                └───chordShell: development environment 'nix-shell'

We can take a look at the flake.lock file:

{
  "nodes": {
        "mypkgs": {
          "inputs": {
                "nixpkgs": "nixpkgs"
          },
          "locked": {
                "lastModified": 1655369898,
                "narHash": "sha256-KjNK0lwGwwpFBwX7cPGg3iERc//rOkUqYzsvobSUCUY=",
                "ref": "master",
                "rev": "1ca007939e4dbcc9ca80830301ea1e120c3fac2d",
                "revCount": 5,
                "type": "git",
                "url": "https://gitlab.inria.fr/qguillot/mypkgs_example"
          },
          "original": {
                "type": "git",
                "url": "https://gitlab.inria.fr/qguillot/mypkgs_example"
          }
        },
        "nixpkgs": {
          "locked": {
                "lastModified": 1655278232,
                "narHash": "sha256-H6s7tnHYiDKFCcLADS4sl1sUq0dDJuRQXCieguk/6SA=",
                "owner": "nixos",
                "repo": "nixpkgs",
                "rev": "8b538fcb329a7bc3d153962f17c509ee49166973",
                "type": "github"
          },
          "original": {
                "owner": "nixos",
                "ref": "nixos-22.05",
                "repo": "nixpkgs",
                "type": "github"
          }
        },
        "nixpkgs_2": {
          "locked": {
                "lastModified": 1653936696,
                "narHash": "sha256-M6bJShji9AIDZ7Kh7CPwPBPb/T7RiVev2PAcOi4fxDQ=",
                "owner": "nixos",
                "repo": "nixpkgs",
                "rev": "ce6aa13369b667ac2542593170993504932eb836",
                "type": "github"
          },
          "original": {
                "owner": "nixos",
                "ref": "22.05",
                "repo": "nixpkgs",
                "type": "github"
          }
        },
        "root": {
          "inputs": {
                "mypkgs": "mypkgs",
                "nixpkgs": "nixpkgs_2"
          }
        }
  },
  "root": "root",
  "version": 7
}

We can see that there are much more information now. There is the information about our mypkgs flake, as well as its inputs (in this case, just nixpkgs). As we have now two versions of nixpkgs the one of the experiments flake and the one of the mypkgs flake, the lock file will save the information both of them for a better traceability.

We can now enter the shell environment by using the nix develop command with the name of the shell.

nix develop .#chordShell