Your First Flake

Note: flakes are still an “experimental” (but recommended) feature of nix. In order to use them you will need to enable them. Write this line to ~/.config/nix/nix.conf:

experimental-features = nix-command flakes

In this example, we’ll instead use something called a flake.

If you are used to using another language’s package manager (like npm or uv or cargo) then you are probably familiar with the pattern of having a dependency file with an associated lockfile. In nix, flake.nix is our dependency file, and evaluating it will produce a flake.lock file for us.

flake.nix
{
  description = "A minimal development environment with bash.";
 
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };
 
  outputs = { self, nixpkgs }:
    let
      system = "aarch64-darwin";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      devShell.${system} = pkgs.mkShell {
        name = "bash-dev";
        buildInputs = [ pkgs.bash ];
        shellHook = ''
          echo "Welcome to the bash development environment!"
          echo "Type 'exit' to leave."
        '';
      };
    };
}

Running nix develop will both evaluate our flake.nix file and also start up a new shell for us using our declared dependencies.

$ nix develop
Welcome to the bash development environment!
Type 'exit' to leave.
$ echo $SHELL
/nix/store/x7m765hh1m4yikg9spw18lxldddvdnd0-bash-5.2p37/bin/bash

Evaluating our new flake.lock file will show us all of our new immutable dependencies.

Just as with other package managers, we could now commit this flake.lock file and be 100% certain that other people using this repository will use exactly the same shell that we are!

{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1740019556,
        "narHash": "sha256-vn285HxnnlHLWnv59Og7muqECNMS33mWLM14soFIv2g=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "dad564433178067be1fbdfcce23b546254b6d641",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "ref": "nixpkgs-unstable",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs"
      }
    }
  },
  "root": "root",
  "version": 7
}

At this point you should feel free to move on to the next example, but if you would like to do a slightly deeper dive into what is happening here, read on!

Let’s start by thinking about our non-nix bash. If you’re running homebrew on a mac, then your output probably looks similar.

$ which bash
  /opt/homebrew/bin/bash
$ otool -L $(which bash)
  /opt/homebrew/bin/bash:
          /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
          /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
          /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 2420.0.0)
          /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.100.2)

What’s that otool call? Well, let’s find out!

An important piece of reproducible builds is dynamically-linked code.

The list that otool gave us is all of the libraries that bash dynamically links to. Part of what this means is that if I were to copy and paste this exact bash binary from my machine to another machine on which those libraries are different (or missing) then my bash might not run or might behave differently.

When nix talks about hermetic builds and pure vs. impure builds this is exactly the kind of impurity that it’s talking about!

$ man otool
The otool-classic command displays specified parts of object files or
libraries. It is the preferred tool for inspecting Mach-O binaries,
especially for binaries that are bad, corrupted, or fuzzed. It is also
useful in situations when inspecting files with new or "bleeding-edge"
Mach-O file format changes.
-L     Display the names and version numbers of the shared libraries that
       the object file uses, as well as the shared library ID if the file
       is a shared library.

What about our nix built bash?

A couple of differences to note:

  • The first two lines from our homebrew bash (libncurses and libiconv) are gone
  • The last two lines (CoreFoundation and libSystem.B) are still there.
  • It might seem surprising that even the nix-built bash links dynamically against MacOS core libraries, but this is normal. Apple has very strict backwards compatibility, so it’s generally safe to link against those dependencies. On Linux we would see different behavior (nix would explicitly link against a nix-provided version of libc).

$ nix-shell
Welcome to the bash development environment!
Type 'exit' to leave.
$ otool -L $(echo $SHELL)
/nix/store/x7m765hh1m4yikg9spw18lxldddvdnd0-bash-5.2p37/bin/bash:
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1775.118.101)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1292.100.5)