Unpacking A Flake

Basically nix flakes only consist of three top level elements, namely:

  • description (optional but recommended)
  • inputs (required for declaring dependencies)
  • outputs (defines what the flake produces)

The description is just a short string, giving users a hint of what the flake is intended for. The inputs on the other hand are attribute sets and outputs is a function that returns an attribute set.

Inputs

inputs = {
  devshell.url = "github:numtide/devshell";
  flake-utils.url = "github:numtide/flake-utils";
  nixpkgs.url = "github:nixos/nixpkgs";
};

flake inputs

The inputs above declare the sources of the desired flakes whose outputs you would like to use inside your own flake. In this case we are using nix packages, flake-utils and devshell. These are the inputs you will find in almost all of the flakes I write as they provide everything I need to get started.

Outputs

Flake are only as useful as their outputs and as such to be of use to anyone it should at a minimum provide a package to build/install/run.

...
outputs = {
  flake-utils,
  devshell,
  nixpkgs,
  ...
}:
...

flake outputs

The outputs attribute defines a function that returns an attribute set. The argument takes the inputs as parameters, we need to declare those to be able to use the functionality provided by the inputs we declared earlier on.

...
flake-utils.lib.eachDefaultSystem (
  system: let
    pkgs = import nixpkgs {
      inherit system;
      overlays = [
        devshell.overlays.default
        (prev: next: {
          hello = hello';
        })
      ];
    };

    hello' = pkgs.stdenvNoCC.mkDerivation {
      name = "hello";
      src = ./bin;
      buildPhase = ''
        mkdir -p $out/bin
        cp ./hello $out/bin
      '';
    };

  in {
    devShells.default = pkgs.devshell.mkShell {
      imports = [
        (pkgs.devshell.importTOML ./devshell.toml)
      ];
    };

    packages.default = hello';

    formatter = pkgs.alejandra;
  }
);
...

the function body

Okay, there is quite a bit going on here, lets start from the beginning.

...
flake-utils.lib.eachDefaultSystem (
  system: let
...

This uses a handy function from flake-utils to cater for all the default supported systems, executing the build for the one you are running on.

...
system: let
  pkgs = import nixpkgs {
    inherit system;
    overlays = [
      devshell.overlays.default
      (prev: next: {
        hello = hello';
      })
    ];
  };

  hello' = pkgs.stdenvNoCC.mkDerivation {
    name = "hello";
    src = ./bin;
    buildPhase = ''
      mkdir -p $out/bin
      cp ./hello $out/bin
    '';
  };
in {
...

Within thelet in block we define pkgs and hello' variables to be used within the flake.

...
in {  
  devShells.default = pkgs.devshell.mkShell {
      imports = [
        (pkgs.devshell.importTOML ./devshell.toml)
      ];
  };

  packages.default = hello';

  formatter = pkgs.alejandra;
}
...

The item following in is in fact what is returned, in this case an attribute set defining devShells, packages and formatter attributes.

  • Devshells are used to provide a way to share developer shells that behave in a predictable and reproducible manner across various developer's machines.
  • The packages section is most likely the item consumers of flakes are most interested in; it provides the artefact(s) the flake is designed to build, package and distribute in the first place.
  • The formatter is defined to enforce what formatter nix will be using when nix fmt is called, formatting all the nix files in a predefined way.

Using The Flake

Running nix develop will target devShells.default, whereas nix develop #.myShell will target devShells.myShell. The exact same goes for nix build and nix run, both would build/run packages.default . When using nix build .#myPackage and nix run .#myPackage the package packages.myPackage would be built and run respectively.

The flake above is available on GitHub. You can also run the default package directly using nix run "github:ck3mp3r/nixamples?dir=hello".
Similarly you can drop into the default developer shell by running nix develop "github:ck3mp3r/nixamples?dir=hello".

One More Thing

Flakes come in pairs, for every flake.nix there is a flake.lock. The lock file ensures that versions are pinned and everyone using the flake has the same versions of inputs and their dependencies. This is analog to a packages.lock for nodejs or cargo.lock for rust.

If things seem a bit confusing, don't worry, it just means now is the right time to learn the nix language before progressing any further. Don't ask me how I know...