This blog series will consist of short, minimal walkthroughs for using Nix to both package existing software and develop new software. The series is aimed at people who have heard of Nix; who may have seen some git repos containing files like default.nix, flake.nix, shell.nix and module.nix; and who maybe have been using NixOS for a little while.

In this series I will cover the purpose of the aforementioned files and how they relate to each other. The following posts are planned:

  1. Building a Nix package using nix-build (this post)
  2. nix-shell: Creating a Nix development shell
  3. Flakes: Building a Nix package using nix build
  4. Flakes: Creating a Nix development shell using nix shell
  5. Creating a NixOS module
  6. Creating a Home Manager module

This list is kept up-to-date for any changes that may occur.

default.nix and nix-build

Suppose you are having a bad day. Wouldn't it be nice to receive an encouraging message from a friendly ASCII-art cow? Well, have I got just the bash script for you.

#!/usr/bin/env bash

messages=(
    "You got this!"
    "Trust me bro it's gonna get better"
    "Everyone has bad days, don't let it get you down!"
)

# This shell script syntax is pretty cursed right?
random_index=$(($RANDOM % ${#messages[@]}))
random_message=${messages[$random_index]}

cowsay $random_message

The script is saved as src/encouraging_cowsay.sh, so let's run it!

bash src/encouraging_cowsay.sh
 _________________________________________
/ Everyone has bad days, don't let it get \
\ you down!                               /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Now we would like to share this wonderful script with our fellow Nix users. In order to do that, we create a file called default.nix with the following contents:

{ pkgs ? (import <nixpkgs> {}) }:

pkgs.stdenv.mkDerivation {
  pname = "encouraging_cowsay";
  version = "0.1";

  src = ./.; # The current directory

  # Dependencies of encouraging_cowsay.sh itself
  buildInputs = with pkgs; [
    cowsay
  ];

  # Packages used while building the Nix package.
  # We use makeWrapper's bash function 'wrapProgram' during the installPhase below.
  nativeBuildInputs = with pkgs; [
    makeWrapper
  ];

  installPhase = ''
    # $out is the Nix store directory for our Nix package
    mkdir -p $out/bin
    cp encouraging_cowsay.sh $out/bin/encouraging_cowsay
    chmod +x $out/bin/encouraging_cowsay

    # This needs further explanation. Read on learn more.
    wrapProgram $out/bin/encouraging_cowsay --prefix PATH : \
      ${pkgs.lib.makeBinPath [ pkgs.cowsay ]}
  '';
}

We can now run nix-build in order to build our package. This generates a directory result in our current working directory, which is a symlink to a directory in the Nix store. Remember $out from the installPhase?

tree -la
.
├── default.nix
├── encouraging_cowsay.sh
└── result -> /nix/store/4y72xfcqlsbb44ij813ybq51mf6np9wp-encouraging_cowsay-0.1
    └── bin
        ├── encouraging_cowsay
        └── .encouraging_cowsay-wrapped

3 directories, 4 files

Creating a wrapper

Did you notice that result/bin contained two files, even though we packaged just one shell script? This is because we have generated a so-called wrapper; a script that will call our encouraging_cowsay.sh script, now renamed to encouraging_cowsay-wrapped, such that our script can find its dependency cowsay in the PATH environment variable. Let's take a look at the newly generated encouraging_cowsay script, i.e. the wrapper:

#! /nix/store/q1c2flcykgr4wwg5a6h450hxbk4ch589-bash-5.2-p15/bin/bash -e
PATH=${PATH:+':'$PATH':'}
PATH=${PATH/':''/nix/store/rc7kpqwb8z5ch37ysv5yk9yg5hl5bkdj-cowsay-3.7.0/bin'':'/':'}
PATH='/nix/store/rc7kpqwb8z5ch37ysv5yk9yg5hl5bkdj-cowsay-3.7.0/bin'$PATH
PATH=${PATH#':'}
PATH=${PATH%':'}
export PATH
exec -a "$0" "/nix/store/4y72xfcqlsbb44ij813ybq51mf6np9wp-encouraging_cowsay-0.1/bin/.encouraging_cowsay-wrapped"  "$@" 

This looks a bit messy, but if we filter it down to its core components, three things happen:

  1. We add cowsay, i.e. encouraging_cowsay's sole dependency, to the variable PATH.
  2. We export PATH as an environment variable.
  3. We use the exec command to run our original encouraging_cowsay script, now present in the Nix store as .encouraging_cowsay-wrapped, while implicitly inheriting the environment variable PATH.
    1. -a "$0" means: execute this command with name $0, i.e. the name of the script: encouraging_cowsay.
    2. $@ means: all positional parameters (e.g. $1, $2, $3, etc.). Hence, we are passing all parameters that were passed to the wrapper script along to the wrapped script.

For reference, /nix/store/af21v8lrn58a0fc3wqxq11ri5y42rljg-encouraging_cowsay-0.1/bin/.encouraging_cowsay-wrapped looks like this:

#!/nix/store/q1c2flcykgr4wwg5a6h450hxbk4ch589-bash-5.2-p15/bin/bash

messages=(
    "You got this!"
    "Trust me bro it's gonna get better"
    "Everyone has bad days, don't let it get you down!"
)

# This shell script syntax is pretty cursed right?
random_index=$(($RANDOM % ${#messages[@]}))
random_message=${messages[$random_index]}

cowsay $random_message

Notice that Nix has converted the shebang to a Nix store path to bash, but has otherwise left our script untouched.

What's next?

I think it'll be no surprise for you to hear that most real-world Nix packages are more complicated than what we have done here. Next time, let's create a Nix development shell such that we can make encouraging_cowsay a bit more interesting!