Nix 101: Part 2
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.