nix-playgraound/notes/003-flakes.md
2026-04-23 11:15:05 +02:00

8.6 KiB

Flakes

A flake is a directory containing flake.nix (and, once evaluated, flake.lock). It's a packaging convention on top of regular Nix that gives you:

  • Pinned inputs via flake.lock for reproducible builds.
  • A fixed output schema so tools know where to look (devShells.<system>.default, packages.<system>.default, and so on).
  • Composability, since flakes depend on other flakes by URL.
  • Pure evaluation, with no silent dependence on NIX_PATH, env vars, or random files.

Flakes are still marked "experimental" but are now the way most new Nix code is written.


1. Anatomy of a flake.nix

{
  description = "…";            # optional human-readable summary

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    # Pattern: make a transitive input follow yours, to avoid duplicates
    flake-utils.url = "github:numtide/flake-utils";
    flake-utils.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, nixpkgs, flake-utils, ... }: {
    # arbitrary attrs here, but names with meaning to `nix` CLI must match the schema
  };
}

Two top-level keys matter: inputs and outputs. description is cosmetic.

Inputs

Each input has a flake reference URL. Common forms:

github:owner/repo                    # latest default branch
github:owner/repo/branch-or-tag      # specific ref
github:owner/repo/abc123             # specific commit
git+https://example.com/repo.git
git+ssh://git@example.com/repo
path:./local-flake                   # local path
tarball+https://…/src.tar.gz
nixpkgs                              # registry alias (resolves via `nix registry`)

Non-flake sources (a repo without flake.nix):

inputs.some-src = { url = "github:owner/repo"; flake = false; };
# Access raw files via self.inputs.some-src

Outputs

A function: { self, ...inputs }: <attrset>. The attrset keys are conventions the nix CLI knows about:

Output path What it is Command
devShells.<system>.<name> A mkShell, a dev environment nix develop [.#<name>]
packages.<system>.<name> A derivation, a build target nix build [.#<name>]
apps.<system>.<name> { type = "app"; program = …; } nix run [.#<name>]
nixosConfigurations.<host> nixpkgs.lib.nixosSystem { … } nixos-rebuild switch --flake .#<host>
homeConfigurations.<user> home-manager config home-manager switch --flake .#<user>
nixosModules.<name> reusable NixOS module imported by other flakes
overlays.<name> nixpkgs overlay imported by other flakes
formatter.<system> a formatter derivation nix fmt
checks.<system>.<name> derivations run by nix flake check nix flake check
templates.<name> scaffold for nix flake init nix flake init -t .#<name>

Default attrs default are picked when you omit the name: nix buildnix build .#default.

.<system> is the platform triple: x86_64-linux, aarch64-linux, aarch64-darwin, or x86_64-darwin.


2. The Lockfile

flake.lock pins the exact commit and NAR hash of every input, transitively. Generated or updated automatically on first use or via nix flake update.

  • Commit it. Without it, "my flake" means different things on different machines.
  • Update explicitly. nix flake update (all inputs) or nix flake update nixpkgs (one input).
  • Inspect. nix flake metadata prints the resolved URLs and revisions.
  • follows deduplicates: if both flake-utils and foo want their own nixpkgs, follows = "nixpkgs"; forces them to use yours. Without this, you can end up with multiple nixpkgs copies and subtle version skew.

3. Multi-System Support

A flake output has to enumerate systems explicitly. Two common patterns:

(a) Manual forAllSystems

outputs = { self, nixpkgs }:
  let
    systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
    forAllSystems = f:
      nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; }));
  in {
    packages = forAllSystems (pkgs: { default = pkgs.hello; });
    devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = [ pkgs.jq ]; }; });
  };

Zero external deps, explicit.

(b) flake-utils / flake-parts

inputs.flake-utils.url = "github:numtide/flake-utils";

outputs = { self, nixpkgs, flake-utils }:
  flake-utils.lib.eachDefaultSystem (system:
    let pkgs = import nixpkgs { inherit system; }; in {
      packages.default = pkgs.hello;
      devShells.default = pkgs.mkShell { packages = [ pkgs.jq ]; };
    });

Less boilerplate, at the cost of one dependency. For larger projects, flake-parts is increasingly popular: it gives you a module system for flakes themselves.


4. Flake URIs: .#<name> Syntax

Everywhere you point nix at a flake, the form is <flake-ref>#<attr>:

nix build .                        # current dir, packages.<sys>.default
nix build .#foo                    # packages.<sys>.foo
nix build github:owner/repo#foo    # remote flake
nix develop .#ci                   # devShells.<sys>.ci
nix run nixpkgs#hello              # pkgs.hello from the nixpkgs registry flake
nixos-rebuild switch --flake .#my-host

The <system> portion is auto-inferred from your machine; you don't spell it out in the CLI.


5. Pure Evaluation: What You Lose, What You Gain

Flakes evaluate in pure mode:

  • No reading NIX_PATH or <nixpkgs>.
  • No arbitrary builtins.getEnv or reading files outside the flake.
  • builtins.currentTime and builtins.currentSystem are blocked.
  • Source is pulled from the flake's input tree, not the working directory, unless you're in self. Files not tracked by git are invisible to the flake by default.

This last point surprises people: git add a file before nix build can see it. (nix build --impure escapes this, but then you're not reproducible.)

In exchange: anyone with your flake.nix plus flake.lock gets bit-identical evaluation.


6. Common Commands

nix flake init                     # scaffold a flake.nix in current dir
nix flake init -t github:nix-community/templates#rust
nix flake show                     # tree of all outputs
nix flake metadata                 # locked inputs + paths
nix flake check                    # evaluate & build all `checks` outputs
nix flake update                   # bump all inputs
nix flake update nixpkgs           # bump one input
nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.11
nix develop                        # enter devShells.<sys>.default
nix develop .#ci                   # named shell
nix build                          # build packages.<sys>.default → ./result
nix run                            # run apps.<sys>.default (or packages.<sys>.default)

7. Recurring Patterns Worth Recognizing

Rust / Go / Python Dev Shell

devShells.default = pkgs.mkShell {
  packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ];
  env.RUST_LOG = "debug";
  shellHook = ''export FOO=bar'';
};

Package a Local Source Tree

packages.default = pkgs.rustPlatform.buildRustPackage {
  pname = "my-tool";
  version = "0.1.0";
  src = ./.;
  cargoLock.lockFile = ./Cargo.lock;
};

Expose a NixOS Module

nixosModules.default = import ./module.nix;
# Consumer does: imports = [ inputs.my-flake.nixosModules.default ];

NixOS System Config as a Flake

nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem {
  system = "x86_64-linux";
  modules = [ ./configuration.nix ];
};
# Apply with: nixos-rebuild switch --flake .#my-laptop

Overlay as an Output

overlays.default = final: prev: {
  my-patched-foo = prev.foo.overrideAttrs (old: {  });
};

8. Debugging Tips

  • nix flake show --all-systems to see outputs for every platform, not just yours.
  • nix eval .#packages.x86_64-linux.default.drvPath to get the .drv without building.
  • nix eval --json .#devShells.x86_64-linux.default.buildInputs | jq to inspect shell deps.
  • --show-trace on any command for full eval backtraces.
  • nix repl . loads your flake's outputs into a REPL: useful for poking at attrs.
  • If an input seems stuck on an old rev, check flake.lock; nix flake update <name> fixes it.

9. When Flakes Are Overkill

  • Running a one-off tool: nix run nixpkgs#ripgrep, no flake needed.
  • A tiny personal script: plain default.nix plus nix-build is still fine.
  • Large monorepos with complex CI: consider flake-parts or hybrid setups (flake for the interface, regular Nix for guts).

Flakes are a packaging interface, not a requirement. The old non-flake world still works; flakes just compose better once you have more than one Nix project.