Nix 101: Part 2

Jul 30, 2024ยท
Christopher Coverdale
Christopher Coverdale
ยท 4 min read

The second part of Nix 101 will use the language features described in Part 1, and create a real life use case.

Creating a Zig package

In this example, we are going to create a derivation of the Zig language.

Specifically, we want to be able to build a derivation where a certain version of Zig can be built e.g. “Give me Zig 0.13.0”.

Derivation

First lets define a derivation.

A derivation is a Nix file that represents the build process of software.

A derivation is a Nix expression that declares the dependencies and required build steps to compile/build the sofware.

The process is reproducible meaning, the same inputs always evaluate to the same outputs. The Nix derivation can also be used as an import in shells or other derivations as a dependency.

Shells

Nix Shells are reproducible environments.

This means that we can use the Nix Language to declaretively express the dependencies of a shell and any required hooks in a reproducible manner.

The shell should evaluate to the same output given the same declared input.

These shells can also be isolated from the underlying environment, creating a pure development environment.

Creating the Derivation

We are going to create an example derivation of the Zig language.

First we are going to start with getting Zig version 0.13.0.

This derivation is in a file named zig.nix.

We import stdenv to be able to call mkDerivation to create the derivation.

fetchurl will download the binary and note the hash sha256. This is the hash of the binary. This is important since it provides a reproducible step, if the downloadable content were to change upstream, we can tell that it wouldn’t evaluate to the same hash. This could be due to a malicious upstream change to the software or an accidental one, either way, this protects our derivation and subsequent use of this package as a dependency.

Note the buildPhase. This is a hook in the derivation, and we are saying at this phase, make a directory at $out/bin and copy the downloaded binary to that path.

$out is a Nix environment variable for the designated output path for the result of the output build.

zig.nix

{ stdenv, fetchurl, }:

stdenv.mkDerivation {
    pname = "zig";
    version = "0.13.0";

    src = fetchurl {
    url = "https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz";
    sha256 = "1FMS5h68xIAyt3vEz3/WkVwR+hbkqtEWtmyUaCESMOo="";
  };

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

We can build this derivation nix-build ./zig.nix and this will build or derivation with a resulting output for Zig 0.13.0.

But let’s say we wanted the option to request different versions of Zig. We can achieve this by passing args to our a derivation.

Let’s create a new nix file, default.nix.

default.nix will allow us to paramertize the build when calling the derivation.

We’re going to pass x2 args:

  • zigVersion
  • zigHash

This will allow us to set the zigVersion we want and it’s corresponding hash.

Notice that we call the package with different versions and hashes. We then assign the evaluated result to a named variable: zig0130 and zig0120.

default.nix

let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
  pkgs = import nixpkgs { config = {}; overlays = []; };
in
  {
    zig0130 = pkgs.callPackage ./zig.nix { zigVersion = "0.13.0"; zigHash = "1FMS5h68xIAyt3vEz3/WkVwR+hbkqtEWtmyUaCESMOo="; };
    zig0120 = pkgs.callPackage ./zig.nix { zigVersion = "0.12.0"; zigHash = "x66Ga4p2pWji1c/TH+ic22Kb3RYf3VAYsppKChcEXK0="; };
  }

We’ll update the zig.nix derivation file to accept the args as variables.

The derivation can now use and build according to the passed zigVersion and corresponding zigHash.

We can also specify which version we want to build: nix-build -A zig0120

zig.nix

{ stdenv, fetchurl, zigVersion, zigHash }:

stdenv.mkDerivation {
    pname = "zig";
    version = zigVersion;

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

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

We can take this another step further by creating a development shell environment where we can use the local derivation: nix-shell --argstr zigVersion "0.13.0"

We can import the local derivation using import ./default.nix and then call the named variable in the evaluated attribute. This will grab the built version we require as a dependency.

shell.nix

{ pkgs ? import <nixpkgs> {},
  zigVersion ? "0.13.0",
}:

let
  customPackages = import ./default.nix;
  selectedZig = if zigVersion == "0.13.0" then
                  customPackages.zig0130
                else if zigVersion == "0.12.0" then
                  customPackages.zig0120
                else
                  customPackages.zig0130;
in
    pkgs.mkShell {
        buildInputs = [ selectedZig ];

        shellHooks = ''
            echo "Running Zig shell with version: ${zigVersion}"
        '';
}

Summary

Nix allows us to declaretively create expressions that evaluate to reproducible build packages and development environments.

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