Simple C program

Many fundamental unix tools are written in C, as it provides many benefits to system programmers. In this section we will cover how to compile and package a simple C application to demonstrate how the nix build process works.

Impure build and install

Given the example C program:

$ cat simple.c
#include <stdio.h>

void main() {
  printf("Hello from Nix!");
}

The build and installation of which on a traditional FHS system may look like:

# build
$ gcc simple.c -o hello_nix
# install
$ sudo cp hello_nix /usr/bin/hello_nix

However, let's see how this would be done in nix

Nix build

Implicit to the previous workflow, was the availability of the GNU C Compiler and the usage of the cp command. In many package repositories, usage of these tools is near universal; and forms the foundation for how to build most other software.

Although C compilers and GNU's coreutils (where cp comes from) have their own specific packages in nixpkgs, generally they are aggregated into a pseudo-package called stdenv in nixpkgs. The function stdenv.mkDerivation provides:

  • A nixpkgs-compatible wrapped C compiler (GCC on linux, Clang on MacOS)
  • GNU coreutils
  • A default "builder" script

stdenv will be covered in more detail in the next section.

A nixified version of the build would look like:

# simple.nix
let
  pkgs = import <nixpkgs> { };
in
  pkgs.stdenv.mkDerivation {
    name = "hello-nix";

    src = ./.;

    # Use $CC as it allows for stdenv to reference the correct C compiler
    buildPhase = ''
      $CC simple.c -o hello_nix
    '';
  }

Nix defaults to a Makefile workflow unless specified otherwise. So stdenv will default to calling make install for the installPhase which will fail with No rule to make target 'install' so we need to also fix how nix will install the package.

$ nix-build simple.nix
this derivation will be built:
  /nix/store/dbavzdq1idb0hvwdh7r9gfn2l52kvycf-hello-nix.drv
...
install flags: SHELL=/nix/store/3j918i1nbwhby0y38bn2r438rjhh8f4d-bash-5.1-p16/bin/bash install
make: *** No rule to make target 'install'.  Stop.
error: builder for '/nix/store/dbavzdq1idb0hvwdh7r9gfn2l52kvycf-hello-nix.drv' failed with exit code 2;

Nix install

The second glaring problem in the old workflow, is that we had a convention as to where to install the executable in /usr/bin/. But installing software in a central location is one the issues that nix is trying to solve. Instead, nix needs to install files on a per-package basis, thus where we need to install files will change for every package. So how do we know where to install files with nix?

Nix will bind the values defined in the derivation to environment variables inside of the nix build. The default "output" of a package is out, which will be bound to the hashed nix store path mentioned in the derivation section.

So an adjusted workflow would be:

# build
$ gcc simple.c -o hello_nix
# install
$ mkdir -p $out/bin
$ cp hello_nix $out/bin/hello_nix

Extending the example above, the easiest solution would be to write our own installPhase. The resulting expression would be:

# simple.nix
let
  pkgs = import <nixpkgs> { };
in
  pkgs.stdenv.mkDerivation {
    name = "hello-nix";

    src = ./.;

    buildPhase = ''
      $CC simple.c -o hello_nix
    '';

    installPhase = ''
      mkdir -p $out/bin
      cp hello_nix  $out/bin/hello_nix
    '';
  }

Now when we build the package, nix is able to realize it. After which we can use the executable:

$ nix-build simple.nix
this derivation will be built:
  /nix/store/9j274i4wckn0ksxpj7asd8vbk67kfz4p-hello-nix.drv
...
/nix/store/giwy9rwzwsdvh86pvdpv37lkwms7xcx9-hello-nix

$ ./result/bin/hello_nix
Hello from Nix!