This post aims to help you getting started with Nix Flakes in order to ease the distribution of reproducible shell environments.
What is Nix Flakes ?
In a previous blog post about nix-shell we have introduced Nix and how to benefit from the nix-shell feature to manage shareable and reproducible shell environments.
However defining an environment using nix-shell lacks of standardization. The new Nix flake standardizes the usage of Nix artifacts. The Nix project provides a new command called nix flake which handles flake.nix files.
How to enable nix flake
To install Nix please refer to the previous blog post.
The flake feature is still considered experimental thus a specific Nix configuration is necessary in ~/.config/nix/nix.conf:
experimental-features = nix-command flakes
A Shell environment described as a Flake
Based on the format definition for a flake we can rewrite our previous simple shell.
{ description = "My-project build environment"; nixConfig.bash-prompt = "[nix(my-project)] "; inputs = { nixpkgs.url = "github:nixos/nixpkgs/22.11"; }; outputs = { self, nixpkgs }: let pkgs = nixpkgs.legacyPackages.x86_64-linux.pkgs; fooScript = pkgs.writeScriptBin "foo.sh" '' #!/bin/sh echo $FOO ''; in { devShells.x86_64-linux.default = pkgs.mkShell { name = "My-project build environment"; buildInputs = [ pkgs.python39 pkgs.python39Packages.tox pkgs.python39Packages.flake8 pkgs.python39Packages.requests pkgs.python39Packages.ipython fooScript ]; shellHook = '' echo "Welcome in $name" export FOO="BAR" ''; }; }; }
Then by running nix develop we enter the shell (devShell).
Note that, when working inside a Git repository, Nix expects that the flake.nix file is known by git (at least staged with git add flake.nix) or it will ignore it.
The nix flake check command can be used to validate the flake file.
$ nix develop Welcome in My-project-build-environment [nix(my-project)] python --version Python 3.9.15 [nix(my-project)] which ipython /nix/store/1kgkssy7lkgsxpjii618ddjq2v03473x-python3.9-ipython-8.4.0/bin/ipython
A flake.nix file must follow a specific format based on the Nix language. The base structure in an attribute set { ... } with specific attributes such as:
- description: a simple string that defines the flake's purpose.
- inputs: an attribute set that defines the flake's dependencies.
- outputs: a function that returns an attribute set with arbitratry attributes. However nix' subcommands expect to find specific attributes in the flake's output. For instance the nix develop expects to find the devShells attribute.
Note that we pin the nixpkgs version to the 22.11 tag by overiding the nixpkgs's url in the input attribute. For better reproducibility, nix creates a flake.lock file to pin dependencies to specific git hashes. This lock file should be distributed along with the flake.nix file.
The nix flake metadata and show subcommands can be used to display flake' dependencies and output.
A flake can be easily shared via a git repository. For instance the Monocle project provides a flake with a devShell output then to get the same development environment than Monocle' developers, then simply run:
# Note that the first run might take long to fetch binary dependencies from the # nix cache and to build unavailable binary dependencies (from the cache). $ nix develop github:change-metrics/monocle
A flake to build the Software Factory website
Our website requires some dependencies available on the system in order to be built. To ensure that each teams' member can build the website locally, without spending time understanding which dependencies are needed and then struggling with versions/incompatibility issues, we provide a flake file.
Here is the flake.nix we are using:
{ description = "sf.io site builder flake"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/22.11"; }; outputs = { self, nixpkgs }: let pkgs = nixpkgs.legacyPackages.x86_64-linux.pkgs; buildScript = pkgs.writeScriptBin "build-site.sh" '' #!/bin/sh pushd src ./blog-htmx.sh ./blog-practical-haskell-use-cases.sh ./blog-introducing-effects.sh ./blog-introducing-functional-programming-to-pythonistas.sh ./blog-sf-resources-in-reason.sh ./blog-nix-shell.sh ./blog-nix-shell-flakes.sh popd pushd website pelican content -o output popd ''; in { devShells.x86_64-linux.default = pkgs.mkShell { name = "Website toolings shell"; buildInputs = [ pkgs.pandoc pkgs.python39Packages.pelican buildScript ]; shellHook = '' echo "Welcome in the nix shell for $name" echo "Run the build-site.sh command to build the website in website/output" echo "Then run: firefox website/output/index.html" ''; }; }; }
It is then really easy to build the website:
$ nix develop $ build-site.sh
Package override
If a specific package version is needed in the shell, then it is possible to override a package' attributes to make a new derivation. For instance, let's say that we need, for some reason, to stick to pelican version 4.7.2 instead of 4.8.0 version provided in nixpkgs 22.11. Then, we can override the current definition in our flake.nix using the overridePythonAttrs function this way:
let pelican = pkgs.python39Packages.pelican.overridePythonAttrs (old: rec { version = "4.7.2"; src = pkgs.fetchFromGitHub { owner = "getpelican"; repo = old.pname; rev = "refs/tags/${version}"; hash = "sha256-ZBGzsyCtFt5uj9mpOpGdTzGJET0iwOAgDTy80P6anRU="; postFetch = '' rm -r $out/pelican/tests/output/custom_locale/posts ''; }; });
and finally use the new pelican derivation in the buildInputs of the mkShell function' attributes.
Note that you might need to set hash to an empty string to force Nix to provide you the new hash to be set in the override.