Nix 101: Part 3

Sep 25, 2024ยท
Christopher Coverdale
Christopher Coverdale
ยท 4 min read

The third part of Nix 101 will explain and go through nix flakes.

Flakes - what are they and why

Nix Flakes are another way to create reproducible environments. They are arguably more reproducible than shells and arethe favoured strategy in creating environments.

Flakes are very similar to shells but they address a problem that shells have with reproducibility.

Flakes improve on Nix Shells by addressing the problem inherent in pulling dependencies from Nixpkgs.

In Nix shells, the upstream version of Nixpkgs is not locked, meaning two engineers using the same shell file might use different versions of Nixpkgs (e.g. nixpkgs 1.2.3 vs nixpkgs 1.2.4). This could result in different versions of the dependencies being pulled, leading to breaking builds or inconsistencies between the different environments.

With Nix Flakes, the version of nixpkgs and source is explicitly declared, this generates a flake.lock file that locks in the version.

Example Flake

Using our zig shell in the previous blog post, we’ll create a flake to lock in the nixpkg dependency version.

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

  outputs = { self, nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };

      # Helper function to build different versions of each zig version.
      zigDerivation = { version, hash } : 
        pkgs.stdenv.mkDerivation {
          pname = "zig";
          version = version;

          src = pkgs.fetchurl {
            url = "https://ziglang.org/download/${version}/zig-linux-x86_64-${version}.tar.xz";
            sha256 = hash;
          };

          buildPhase = ''
              mkdir -p $out/bin
              cp ./zig $out/bin
          '';
        };
    in
      {

        packages = {
          x86_64-linux.zig0130 = zigDerivation { version = "0.13.0"; hash = "1FMS5h68xIAyt3vEz3/WkVwR+hbkqtEWtmyUaCESMOo="; };
          x86_64-linux.zig0120 = zigDerivation { version = "0.12.0"; hash = "x66Ga4p2pWji1c/TH+ic22Kb3RYf3VAYsppKChcEXK0="; };
        };

        devShells.x86_64-linux.default = pkgs.mkShell {
          buildInputs = [ self.packages.${system}.zig0130 ];
        };

        devShells.x86_64-linux.zig0130 = pkgs.mkShell {
          buildInputs = [ self.packages.${system}.zig0130 ];
        };

        devShells.x86_64-linux.zig0120 = pkgs.mkShell {
          buildInputs = [ self.packages.${system}.zig0120 ];
        };

      };
}

There’s a lot more going on than the shell, we’ll step through each line:

  • inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - This line declares the upstream source which will contain the version of nixpkgs for this flake

  • We create the derivation and build phase for the flake. We are downloading and going to use the parameters of version and hash when creating this derivation

  • This will download the binary using fetchurl and copy the binary to the $out/bin directory, which is the environment for the shell

  • Important to note, this is still not totally reproducible since this flake would only work correctly on systems that are x86_64-linux

  outputs = { self, nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };

      # Helper function to build different versions of each zig version.
      zigDerivation = { version, hash } : 
        pkgs.stdenv.mkDerivation {
          pname = "zig";
          version = version;

          src = pkgs.fetchurl {
            url = "https://ziglang.org/download/${version}/zig-linux-x86_64-${version}.tar.xz";
            sha256 = hash;
          };

          buildPhase = ''
              mkdir -p $out/bin
              cp ./zig $out/bin
          '';
        };
    in
      {
  • After definig the inputs (the derivation, system and pkgs) we create the zig derivation for certain versions (0.13.0, 0.12.0)
  • We create the shells for each version, using the packages built from the derivations
    in
      {

        packages = {
          x86_64-linux.zig0130 = zigDerivation { version = "0.13.0"; hash = "1FMS5h68xIAyt3vEz3/WkVwR+hbkqtEWtmyUaCESMOo="; };
          x86_64-linux.zig0120 = zigDerivation { version = "0.12.0"; hash = "x66Ga4p2pWji1c/TH+ic22Kb3RYf3VAYsppKChcEXK0="; };
        };

        devShells.x86_64-linux.default = pkgs.mkShell {
          buildInputs = [ self.packages.${system}.zig0130 ];
        };

        devShells.x86_64-linux.zig0130 = pkgs.mkShell {
          buildInputs = [ self.packages.${system}.zig0130 ];
        };

        devShells.x86_64-linux.zig0120 = pkgs.mkShell {
          buildInputs = [ self.packages.${system}.zig0120 ];
        };

      };
}

And finally, to run the dev shell within the flake:

// example: nix develop <path-to-flake-folder>/#<shell-name>

nix develop ./zig-flake/#zig0130

A flake.lock file will get generated after running the flake:

{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1722185531,
        "narHash": "sha256-veKR07psFoJjINLC8RK4DiLniGGMgF3QMlS4tb74S6k=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "52ec9ac3b12395ad677e8b62106f0b98c1f8569d",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "ref": "nixos-unstable",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs"
      }
    }
  },
  "root": "root",
  "version": 7
}

Summary

Flakes solve a reproducibility issue in nix shells by pinning the upstream nixpkg version, allowing different engineers to run the same flake and ensure they have the exact same dependencies.

Did you find this page helpful? Consider sharing it ๐Ÿ™Œ