Skip to content

Intro to Flakes

Note

Nix is in a period of transition. Commands like nix-shell, nix-env and other commands of the form nix-<smth> represent the old way of doing things.

TL;DR flakes are the future of nix.

A flake is simply a directory containing at least a flake.nix file, which describes the flake itself, and a flake.lock file, which is a JSON document describing the exact version of every input.

The flake.nix references one or more inputs (~ dependencies, themselves flakes). nixpkgs, which provides the base repository of 80,000 package definitions, is itself a flake. The flake also describes one or more outputs which it produces or makes available. These can be, but are not limited to:

  • Development Environments
    • defines a shell environment with packages, environment variables and automatically run shell hooks
  • Flake templates
    • akin to project boiler-plates / starter projects in other languages
  • Nix packages
    • Software and resources to make available for dev environments or direct execution
  • NixOS configurations
    • declarative configurations of a NixOS machine. Packages, users, network configuration, services enabled etc.
  • (Library) functions
    • Nix functions exposed to flakes using the flake as an input.

Why use Nix (flakes)?

  1. Isolated development environments Flake's can specify one or more development environments. A development environment consists of some number of packages to install and may run hooks on entering the environment to run commands, set environment variables and so on. This allows development on projects without polluting the system's global environment and environments may use different versions of the same librarie(s).

  2. Flakes are composable I may write a flake which defines derivations (package build scripts) for one or more pieces of software which are not yet available in NixPkgs, or which are internal to our organization. Your flake may then reference these as inputs and install those packages.

  3. Distribution via git - no package repo server/docker registry necessary A flake can be referenced and used directly from a Git repository. This effectively makes it easy to create private "repositories" of customized or internal software without needing to stand up a traditional Debian/Fedora package repository and to configure each client PC to use these repositories.

Our first flake

The following flake (flake.nix) is truly minimal, it only references the nixpkgs flake, pinning it to the 22.11 release, and we define no outputs:

a minimal flake
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
  };

  outputs = { self, nixpkgs}: {

  };
}

Pinning nixpkgs to a fixed release helps us retain reproducibility - github:NixOS/nixpkgs points to the master branch, which is a moving target, while the nixos-22.11 branch is not.

Warning

If you don't specify a release branch for nixpkgs in your flakes, you will be pulling from master, with the flake.lock file reflecting the last commit at the time of the lock file's creation.

This means you can still share your flake with others and they will get the exact same packages as you, but package derivations in master may not yet have been built by NixOS's build server and cached at cache.nixos.org. This means your computer would potentially be recompiling a lot of packages. I tried this with firefox, which definitely taxed my laptop..!

A 'hello world' flake

  1. Create a new directory, hello-world, and enter into it.
  2. Run nix flake init to create a new skeleton flake.nix file
  3. Add the inputs attribute set, defining where to find nixpkgs and which version to use
    • Not necessary, but we want good habits!
  4. Run nix flake lock to generate a lock file
    • Note this would otherwise happen when you first use the flake

Warning

Never commit a flake.nix file to a git repo without a corresponding flake.lock file - the point is to allow your users to replicate what you have. If you desire randomness and despair, just ship tarballs with makefiles!

We now have a flake like this one:

flake.nix - from nix flake init
{
  description = "A very basic flake";

  # This we added ourselves, remember..?
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11";
  };

  outputs = { self, nixpkgs }: {

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

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

  };
}

This flake exposes one program, hello and makes it possible to run using nix run. Let's try running it now:

    $ nix-run .#hello
    Hello, world!

Note

.#hello - what is that? This is odd at first, but is part of a general concept of how to refer to flakes and outputs within them.

In this case, . is the current directory, and flakes on the local file system can be referred to by their path. The hash # sign separates the flake reference from the output attribute in the same way a / would in an URL. Finally, we refer to the hello output attribute.

Note

A note on system: this example's package entries start with packages.x86_64-linux. The x86_64-linux-part is called the system. x86_64-linux refers to a x86 64bit computer running linux. Similarly, aarch64-darwin refers to a 64bit Arm computer running macOS.

As we will see, there are helper functions which allow you to loop over several systems and define the same outputs for all. For now, just be aware of its meaning.