Using Nix Flakes on my projects

Since its introduction to the Nix ecosystem, the usage of Flakes has been steadily increasing. Almost all projects I have been working on in the past 5 years include a flake.nix at the root of the repository, defining packages, modules and development environments.1

One of these projects is a Go tool that I wrote in 2019, and it still has a working environment when I call nix develop or nix build. However, I noticed that the flake.nix file has a significantly different structure compared to the most recent ones.

Let's take a look into it starting with the description. Usually a small string that defines what this flake represents.

description = "foobarer - a project that foos the bar."

After the description, the next step consists of defining the external dependencies of our flake. These are called inputs and usually consist of the unstable nixpkgs branch and devenv on my projects.

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  devenv.url = "github:cachix/devenv";
  devenv.inputs.nixpkgs.follows = "nixpkgs";
};

When I first started using Flakes, the <dependency>.inputs.nixpkgs.follows attribute confused me as it was not clear what it was doing. It overrides the nixpkgs attribute on devenv's flake and tells it to follow our own nixpkgs. We basically propagate our current nixpkgs state to any references of it on our dependencies.

The only missing part now are the outputs. We declare the "public API" of our flake file here. This section defines packages, development environments (also called shells), modules and more.

outputs = { self, nixpkgs, devenv, ... } @ inputs:
  let
    forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
  in {
    # our code comes here
  };

There are a handful of default outputs defined on the flake schema. Some of them are:

  • packages.<system>.<name>
  • devShells.<system>.<name>

The forAllSystems function helps generate these attributes so we don't need to declare the system multiple times. We accomplish this through the lib.genAttrs function receiving lib.systems.flakeExposed. But... how do we use that? Let's declare a small python environment to learn how.

devShells = forAllSystems (system:
  let
    pkgs = nixpkgs.legacyPackages."${system}";
  in {
    default = devenv.lib.mkShell {
      inherit inputs pkgs;
      modules = [
	    ({ pkgs, ... }: {
          languages.python = {
            enable = true;
            venv.enable = true;
            venv.requirements = builtins.readFile ./requirements.txt;
          };
        })
      ];
    };
  });

This is enough to give us a full python environment when we run nix develop --impure on our shell. Thankfully, devenv takes care of most of the complexity for each language setup! It's been a long time since I configured anything for a new programming environment.

  1. The distributed nature of Flakes is one of its strongest features.