Comparing Nix Flakes to Traditional Nix

2025-05-05

Comparing Flakes to Traditional Nix

Flakes

TL;DR These are notes following the Nix-Hour #4, if you would rather just watch a YouTube video I share it at the end. This doesn't follow exactly but I put it together in a way I found easier to follow, it's long but has a lot of great insights for learning more about how NixOS works. It mainly compares how to get pure build results from both Traditional Nix and Flakes.

One of the primary benefits of Nix Flakes is their default enforcement of pure evaluation, leading to more reproducible and predictable builds. In Nix, an impure operation or value depends on something outside of the explicit inputs provided to the build process. This could include things like the user's system configuration, environment variables, or the current time. Impurity can lead to builds that produce different results on different systems or at different times, undermining reproducibility.

In this section, we will compare how Flakes and traditional Nix handle purity and demonstrate the steps involved in building a simple hello package using both methods.

We'll start by creating a hello directory:

mkdir hello && cd hello/

now create a flake.nix:

{
  outputs = { self, nixpkgs }: {
    myHello = (import nixpkgs {}).hello;
  };
}

nix build .#myHello will fail.

To get around this you can pass:

 nix build .#myHello --impure

Let's explore some ways to make this flake build purely.

To do this we need to add the system attribute (i.e. x86_64-linux) with your current system, flake-utils simplifies making flakes system agnostic:

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

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        packages.myHello = pkgs.hello;
      }
    );
}

This will allow it to successfully build with nix build .#myHello because flake-utils provides the system attribute.

Traditional Nix

Create another directory named hello2 and a default.nix with the following contents:

{ myHello = (import <nixpkgs> { }).hello; }

Build it with:

nix-build -A myHello

We can see that it's impure with the nix repl:

nix repl
nix-repl> <nixpkgs>
/nix/var/nix/profiles/per-user/root/channels/nixos

We want to use the same revision for traditional nix for nixpkgs as we did for our nix flake. To do so you can get the revision # from the flake.lock file in our hello directory. You could cd to the hello directory and run cat flake.lock and look for:

    "nixpkgs": {
      "locked": {
        "lastModified": 1746372124,
        "narHash": "sha256-n7W8Y6bL7mgHYW1vkXKi9zi/sV4UZqcBovICQu0rdNU=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0",
        "type": "github"
      },

let
  nixpkgs = fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0.tar.gz";
  };
in {
  myHello = (import nixpkgs {}).hello;
}
let
  nixpkgs = fetchTarball {
    url =
      "https://github.com/NixOS/nixpkgs/archive/0243fb86a6f43e506b24b4c0533bd0b0de211c19.tar.gz";
    sha256 = "0000000000000000000000000000000000000000000000000000";
  };
in { myHello = (import nixpkgs { }).hello; }

You can see that they produce the same result by running:

And they all have defaults that are impure.

Users have problems because they don't realize that defaults are pulled in and they have some overlays and config.nix that are custom to their setup. This can't happen in flakes because they enforces this. We can override this by passing empty lists and attribute sets and a system argument to the top-level function with a default like so:

{system ? builtins.currentSystem}:
let
  nixpkgs = fetchTarball {
    url =
      "https://github.com/NixOS/nixpkgs/archive/0243fb86a6f43e506b24b4c0533bd0b0de211c19.tar.gz";
    sha256 = "1qvdbvdza7hsqhra0yg7xs252pr1q70nyrsdj6570qv66vq0fjnh";
  };
in { myHello = (import nixpkgs {
    overlays = [];
    config = {};
    inherit system;
  }).hello;
}

if you import this file from somewhere else:

import ./default.nix { system = "x86_64-linux"; }

or from the cli:

nix-build -A myHello --argstr system x86_64-linux

or if you already have the path in your store you can try to build it with:

nix-build -A myHello --argstr system x86_64-linux --check

Get the rev from git log:

nix-instantiate --eval --pure-eval --expr 'fetchGit { url = ./.; rev = "b4fe677e255c6f89c9a6fdd3ddd9319b0982b1ad"; }'

Output: { lastModified = 1746377457; lastModifiedDate = "20250504165057"; narHash = "sha256-K6CRWIeVxTobxvGtfXl7jvLc4vcVVftOZVD0zBaz3i8="; outPath = "/nix/store/rqq60nk6zsp0rknnnagkr0q9xgns98m7-source"; rev = "b4fe677e255c6f89c9a6fdd3ddd9319b0982b1ad"; revCount = 1; shortRev = "b4fe677"; submodules = false; }

nix repl
nix-repl> :l <nixpkgs>
nix-repl> hello.outPath
"/nix/store/a7hnr9dcmx3qkkn8a20g7md1wya5zc9l-hello-2.12.1"
nix-repl> "${hello}"
"/nix/store/a7hnr9dcmx3qkkn8a20g7md1wya5zc9l-hello-2.12.1"
nix-repl> attrs = { outPath = "foo"; }
nix-repl> "${attrs}"
"foo"
❯: git log
commit b4fe677e255c6f89c9a6fdd3ddd9319b0982b1ad (HEAD -> main)
nix-build --pure-eval --expr '(import (fetchGit { url = ./.; rev = "b4fe677e255c6f89c9a6fdd3ddd9319b0982b1ad"; }) { system = "x86_64-linux"; }).myHello'

Back to Flakes

If we want to build the flake with a different Nixpkgs:

nix build .#myHello --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.11
result/bin/hello --version

We can't really do this with our default.nix because it's hard-coded within a let statement.

A common way around this is to write another argument which is nixpkgs:

{
  system ? builtins.currentSystem,
  nixpkgs ?
    fetchTarball {
      url = "https://github.com/NixOS/nixpkgs/archive/f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0.tar.gz";
      sha256 = "1mbl5gnl40pjl80sfrhlbsqvyf7pl9r92vvdc43nivnblrivrdcz";
    },
  pkgs ?
    import nixpkgs {
      overlays = [];
      config = {};
      inherit system;
    },
}: {
  myHello = pkgs.hello;
}

Build it:

nix-build -A myHello

or

nix-build -A myHello --arg nixpkgs 'fetchTarball { url =
"https://github.com/NixOS/nixpkgs/archive/f5cbfa4dbbe026c155cf5a9204f3e9121d3a5fe0.tar.gz"; }'`

Or another impure command that you can add purity aspects to, Traditional Nix has a lot of impurities by default but in almost all cases you can make it pure:

nix-build -A myHello --arg channel nixos-24.11

Update the Nixpkgs version in flakes

nix flake update
warning: Git tree '/home/jr/nix-hour/flakes' is dirty
warning: updating lock file '/home/jr/nix-hour/flakes/flake.lock':
 Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/0243fb86a6f43e506b24b4c0533bd0b0de211c19?narHash=sha256-0EoH8DZmY3CKkU1nb8HBIV9RhO7neaAyxBoe9dtebeM%3D' (2025-01-17)
  'github:NixOS/nixpkgs/0458e6a9769b1b98154b871314e819033a3f6bc0?narHash=sha256-xj85LfRpLO9E39nQSoBeC03t87AKhJIB%2BWT/Rwp5TfE%3D' (2025-01-18)
nix build .#myHello

Doing this with Traditional Nix is pretty easy with niv:

nix-shell -p niv
niv init

In our default.nix:

{ system ? builtins.currentSystem,
sources ? import nix/sources.nix,
nixpkgs ? sources.nixpkgs,
pkgs ? import nixpkgs {
  overlays = [ ];
  config = { };
  inherit system;
}, }: {
  myHello = pkgs.hello;
}

Build it:

nix-build -A myHello

niv can do much more, you can add a dependency with github owner and repo:

niv add TSawyer87/system
niv drop system
niv update nixpkgs --branch=nixos-unstable
nix-build -A myHello

The flake and default.nix are both using the same store object:

 nix-build -A myHello
unpacking 'https://github.com/NixOS/nixpkgs/archive/5df43628fdf08d642be8ba5b3625a6c70731c19c.tar.gz' into the Git cache...
/nix/store/a7hnr9dcmx3qkkn8a20g7md1wya5zc9l-hello-2.12.1
 ls -al
drwxr-xr-x    - jr 18 Jan 10:01  .git
drwxr-xr-x    - jr 18 Jan 10:01  nix
lrwxrwxrwx    - jr 18 Jan 10:17  result -> /nix/store/a7hnr9dcmx3qkkn8a20g7md1wya5zc9l-hello-2.12.1

Adding Home-Manager

Flakes:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
    home-manager.url = "github:nix-community/home-manager";
  };

  outputs = { self, nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in { packages.myHello = pkgs.hello; });
}
nix flake update
nix flake show github:nix-community/home-manager

Traditional Nix:

niv add nix-community/home-manager
nix repl
nix-repl> s = import ./nix/sources.nix
nix-repl> s.home-manager

We can follow the outPath and see that there's a default.nix, flake.nix, flake.lock and much more. In the default.nix you'll see a section for docs.

If we want to build the docs go back to our default.nix:

{ system ? builtins.currentSystem, sources ? import nix/sources.nix
, nixpkgs ? sources.nixpkgs, pkgs ? import nixpkgs {
  overlays = [ ];
  config = { };
  inherit system;
}, }: {
  homeManagerDocs = (import sources.home-manager { pkgs = pkgs; }).docs;

  myHello = pkgs.hello;
}

Build it:

nix-build -A homeManagerDocs

With the flake.nix to do this you would add:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
    home-manager.url = "github:nix-community/home-manager";
  };

  outputs = { self, nixpkgs, flake-utils, home-manager, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in {
        packages.myHello = pkgs.hello;
        packages.x86_64-linux.homeManagerDocs =
          home-manager.packages.x86_64-linux.docs-html;
      });
}

Build it:

nix build .#myHello

home-manager.inputs.nixpkgs.follows = "nixpkgs";