< Previous post | Next post >

[2025-03-07][Nix]

Building software using Nix

Today, we are going to learn how to build a package that is not available on nixpkgs. To do this we will write what's called a derivation, it can be viewed as a recipe on how to build a software.

In this tutorial we are going to use the standard environment and I heavily encourage you to take a look at this nice documentation. For this tutorial I will be building starpu, with the documentation available here to build the software from source.

Let's start!

First we will gather all our dependencies to your software, from the StarPU doc I can read: "The hwloc topology discovery library is not mandatory to use StarPU, but strongly recommended. [...] Make sure to not only install a hwloc or libhwloc package, but also hwloc-devel or libhwloc-dev to have hwloc headers etc."

We can search for those packages on nikpkgs search

Note that only hwloc is found, but not libhwloc, hwloc-devel or libhwloc-dev. Generally in nixpkgs, dev and lib packages are included in the main package, here hwloc. We can see that by clicking on source 📦 and by looking at the outputs attribute:

outputs = [
  "out" # binary
  "lib" # library
  "dev" # development headers
  "doc" # documentation
  "man" # manual accesed via "man"
];

So in our case, importing hwloc only will be sufficient.

Now let's write it in the derivation parameter. Nix will automatically match and find existing packages.

# starpu.nix
{
  # standard environment
  stdenv,

  # starpu dependencies
  hwloc,
  fftw,
  fftwFloat,
  pkg-config,
}:

{ }: contains all the parameters that will be passed to the body of the derivation.

Now let's write the body:

# starpu.nix
# ...
stdenv.mkDerivation (finalAttrs: {
  pname = "starpu";
  version = "1.4.7";

  src = fetchzip {
    url = "https://files.inria.fr/starpu/starpu-${finalAttrs.version}/starpu-${finalAttrs.version}.tar.gz";
    hash = "";
  };
}
# ...
)

We write the package name, its version and how the source is fetched. In my case the source can be downloaded as an archive with a link of the form: https://files.inria.fr/starpu/starpu-/starpu-.tar.gz, so we replace with ${finalAttrs.version} so the value of the version attribute defined above can be evaluated.

Remarks:

Alright, now let's try to run derivation. We use nix-shell -E 'with import { }; callPackage ./package.nix { }' in order to run a shell with access your local channel of nixpkgs and call the package that we are writting. You can also use a upstream version of nixpkgs using with import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz") { }.

andrew@nixos /t/starpu> nix-shell -E 'with import  { }; callPackage ./starpu.nix { }'
error:
       … while evaluating a branch condition
         at /nix/store/s44pljdf0z0kqq0pjrf5barx3scf5mdc-nixos/nixos/lib/customisation.nix:305:5:
          304|     in
          305|     if missingArgs == { } then
             |     ^
          306|       makeOverridable f allArgs

       … while calling the 'removeAttrs' builtin
         at /nix/store/s44pljdf0z0kqq0pjrf5barx3scf5mdc-nixos/nixos/lib/attrsets.nix:647:5:
          646|     set:
          647|     removeAttrs set (filter (name: ! pred name set.${name}) (attrNames set));
             |     ^
          648|

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: undefined variable 'fetchzip'
       at /tmp/starpu/package.nix:15:9:
           14|
           15|   src = fetchzip {
             |         ^
           16|     url = "https://files.inria.fr/starpu/starpu-${finalAttrs.version}/starpu-${finalAttrs.version}.tar.gz";

Ok so it seems that we have our first errors! Generally the most useful information is located at the end, here we just forgot to import fetchzip, so we should add it at in the parameters.

# starpu.nix
{
  fetchzip, # <--- Added fetchzip
  stdenv,

  # starpu dependencies
  hwloc,
  fftw,
  fftwFloat, # Same than previous but with float precision
  pkg-config,
}:

stdenv.mkDerivation (finalAttrs: {
  pname = "starpu";
  version = "1.4.7";

  src = fetchzip {
    url = "https://files.inria.fr/starpu/starpu-${finalAttrs.version}/starpu-${finalAttrs.version}.tar.gz";
    hash = "sha256-6AjQr+2nSJ/uYjJ6II4vJgxj5nHuvlsLvPGZZv/cU8M=";
  };
})

And now we should end up in the nix-shell:

andrew@nixos /t/starpu> nix-shell -E 'with import  { }; callPackage ./package.nix { }'

[nix-shell:/tmp/starpu]$

Now that we are in the nix-shell, we will be able to go through the build process and debug if necessary. By default, mkDerivation perform basic build commands such as ./configue; make; make install etc.

They are defined in phases that you can run manually from the nix-shell where we debug the build of our software.

Unpack phase

[nix-shell:/tmp/starpu]$ echo "src = $src" && cd $(mktemp -d) && unpackPhase && cd *
src = /nix/store/bq5l0mmqy55qyfr6a4v54j3jf2h9xl7h-source
unpacking source archive /nix/store/bq5l0mmqy55qyfr6a4v54j3jf2h9xl7h-source
source root is source

[nix-shell:/tmp/nix-shell-30567-0/tmp.g5cNCxrGAI/source]$ ls
aclocal.m4  bubble     configure     COPYING.LGPL    examples  julia  Makefile.am  mpi         README.md      src                 starpupy         STARPU-VERSION  tools
AUTHORS     build-aux  configure.ac  doc             include   m4     Makefile.in  packages    sc_hypervisor  starpufft           STARPU-REVISION  tests
autogen.sh  ChangeLog  contrib       eclipse-plugin  INSTALL   make   min-dgels    README.dev  socl           starpu_openmp_llvm  starpurm         TODO

[nix-shell:/tmp/nix-shell-30567-0/tmp.g5cNCxrGAI/source]$ 

Here we create a temporary directory in /tmp and run unpackPhase which will download the source code.

Configure phase

If you don't have any ./configure file this step is skipped.

[nix-shell:/tmp/nix-shell-30567-0/tmp.g5cNCxrGAI/source]$ configurePhase 
fixing libtool script ./build-aux/ltmain.sh
./configure
...
configure: error: libhwloc or pkg-config was not found on your system. If the target machine is hyperthreaded the performance may be impacted a lot.  It is strongly recommended to install libhwloc and pkg-config. However, if you really want to use StarPU without enabling libhwloc, please restart configure by specifying the option '--without-hwloc'.

The configure phase will perform multiple things which includes running ./configure. Here it fails and starpu tells us that libhwloc or pkg-config is missing, however we wrote them in the parameters why isn't it working? Well it's because a derivation parameter can be a package, like hwloc, but also user defined variables which are not packages. So parameters are not directly exposed to the build environment. For that you need to define either buildInputs or nativeBuildInputs attributes.

# starpu.nix
{
  fetchzip,
  stdenv,

  # starpu dependencies
  hwloc, # <--- imported here
  fftw,
  fftwFloat,
  pkg-config,
}:

stdenv.mkDerivation (finalAttrs: {

  # ...

  nativeBuildInputs = [
    pkg-config
    hwloc # <--- used here
  ];

  buildInputs = [
    fftw
    fftwFloat
    hwloc # <--- used here
    pkg-config
  ];
})

buildInputs specifies 'regular' build dependencies, and nativeBuildInputs specifies the runtime build dependencies i.e. executables that needs to be run at build time. In my case hwloc and pkg-config need to be in both.

[nix-shell:/tmp/nix-shell-47123-0/tmp.sHffGgQ9ff/source]$ configurePhase
...
	StarPU Extensions:
	      StarPU MPI enabled:                            no
	      StarPU MPI failure tolerance:                  no
	      StarPU MPI failure tolerance stats:            no
	      StarPU MPI(nmad) enabled:                      no
	      MPI test suite:                                no
	      Master-Slave MPI enabled:                      no
	      Master-Slave TCP/IP enabled:                   no
	      FFT Support:                                   yes
	      Resource Management enabled:                   no
	      Python Interface enabled:                      no
	      OpenMP runtime support enabled:                yes
	      OpenMP LLVM runtime support enabled:           no
	      Parallel Worker support enabled:               yes
	      SOCL enabled:                                  no
	      SOCL test suite:                               no
	      Scheduler Hypervisor:                          no
	      simgrid enabled:                               no
	      ayudame enabled:                               no
	      HDF5 enabled:                                  no
	      Native fortran support:                        no
	      Native MPI fortran support:                    no
	      Support for multiple linear regression models: no
	      Hierarchical dags support:                     no
	      JULIA enabled:                                 no

Now it works!

Build phase

If your build phase doesn't consist of calling make && make check etc. you can redefine what done in the phase with the following:

# starpu.nix
stdenv.mkDerivation (finalAttrs: {
  # ...
  buildPhase = ''
    # Maybe you are using meson instead of make?
    meson setup builddir
    meson compile -C builddir
  '';
  # ...
})

In my case we're using make so no need to redifine this phase. However we do face another problem:

[nix-shell:/tmp/nix-shell-47123-0/tmp.sHffGgQ9ff/source]$ buildPhase
../doc/extractHeadline.sh
/nix/store/4k90qpzh1a4sldhnf7cxwkm9c0agq4fp-bash-interactive-5.2p37/bin/bash: line 1: ../doc/extractHeadline.sh: cannot execute: required file not found
make[2]: *** [Makefile:1201: README.org] Error 127
make[2]: Leaving directory '/tmp/nix-shell-47123-0/tmp.sHffGgQ9ff/source/doc'
make[1]: *** [Makefile:900: all-recursive] Error 1
make[1]: Leaving directory '/tmp/nix-shell-47123-0/tmp.sHffGgQ9ff/source/doc'
make: *** [Makefile:1181: all-recursive] Error 1

The build crashes upon calling the file extractHeadline.sh, apparently at line 1 the required file isn't found.

[nix-shell:/tmp/nix-shell-93120-0/tmp.IdK5kuMv40/source/doc]$ cat extractHeadline.sh 
#!/bin/bash
# StarPU --- Runtime system for heterogeneous multicore architectures.
# ...

This is a common problem when working with shell scripts on nix. Indeed, /bin/bash doesn't exists, because every programs are stored in /nix/store//bin/bash. You can find where bash actually is with whereis command:

[nix-shell:/tmp/nix-shell-93120-0/tmp.IdK5kuMv40/source/doc]$ whereis bash
bash: /nix/store/4k90qpzh1a4sldhnf7cxwkm9c0agq4fp-bash-interactive-5.2p37/bin/bash /nix/store/11ciq72n4fdv8rw6wgjgasfv4mjs1jrw-bash-5.2p37/bin/bash /nix/store/r3mc0hsvq0vn9b0cqw4r57b7iqk6gnyj-system-path/bin/bash

Of course, we won't just copy the path into our shell script, instead we will use an utility command called patchShebangs that will do that for us.

# starpu.nix
stdenv.mkDerivation (finalAttrs: {
  # ... 
  postConfigure = ''
    patchShebangs --build doc/extractHeadline.sh doc/fixLinks.sh
  '';
  # ... 
  })

We call the command in the postConfigure attribute so it is run after configurePhase and before buildPhase.

[nix-shell:/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source]$ buildPhase
...
  CC       starpufft.lo
  CC       starpufftf.lo
  CC       starpufft_common.lo
  CCLD     libstarpufft-1.4.la
make[2]: Leaving directory '/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source/starpufft/src'
make[2]: Entering directory '/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source/starpufft'
make[2]: Nothing to be done for 'all-am'.
make[2]: Leaving directory '/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source/starpufft'
make[1]: Leaving directory '/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source/starpufft'
make[1]: Entering directory '/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source'
make[1]: Leaving directory '/tmp/nix-shell-111793-0/tmp.UUFUT5WUHV/source'

Now it works, notice that the bash path have been changed in our script:

[nix-shell:/tmp/nix-shell-93120-0/tmp.IdK5kuMv40/source/doc]$ cat extractHeadline.sh 
#!/nix/store/4k90qpzh1a4sldhnf7cxwkm9c0agq4fp-bash-interactive-5.2p37/bin/bash
# StarPU --- Runtime system for heterogeneous multicore architectures.
# ...

And finally you can call installPhase (optionnaly checkPhase to make sure everything is working), and verify that you can use the binaries/libraries/headers. We obtain this minimal file:

# starpu.nix
{
  fetchzip,
  stdenv,

  # starpu dependencies
  hwloc,
  fftw,
  fftwFloat,
  pkg-config,
}:

stdenv.mkDerivation (finalAttrs: {
  pname = "starpu";
  version = "1.4.7";

  src = fetchzip {
    url = "https://files.inria.fr/starpu/starpu-${finalAttrs.version}/starpu-${finalAttrs.version}.tar.gz";
    hash = "sha256-6AjQr+2nSJ/uYjJ6II4vJgxj5nHuvlsLvPGZZv/cU8M=";
  };

  nativeBuildInputs = [
    pkg-config
    hwloc
  ];

  buildInputs =
  [
    fftw
    fftwFloat
    hwloc
    pkg-config
  ];

  postConfigure = ''
    patchShebangs --build doc/extractHeadline.sh doc/fixLinks.sh
  '';
})

We are now able to import the derivation elsewhere, for example in a nix-shell.

let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz") { };
  starpu = pkgs.callPackage ./starpu.nix { };
in
  pkgs.mkShell {
    packages = [ starpu ];
  }

Every phase will be run automatically. What's very cool is that everything will be linked and exposed to the path correctly. For example every binary is in the path, the headers can be included seamlessly and the libraries should be found.

[nix-shell:/tmp/starpu]$ echo $PATH
/nix/store/y0pvdv94v1bwlfm6rv4iy78p94fp8jb6-starpu-1.4.7/bin ...

[nix-shell:/tmp/starpu]$ echo $NIX_CFLAGS_COMPILE 
-frandom-seed=cz96p1h66b -isystem /nix/store/y0pvdv94v1bwlfm6rv4iy78p94fp8jb6-starpu-1.4.7/include -isystem /nix/store/y0pvdv94v1bwlfm6rv4iy78p94fp8jb6-starpu-1.4.7/include

[nix-shell:/tmp/starpu]$ echo $NIX_LDFLAGS 
-rpath /nix/store/cz96p1h66bq5dkc00d33h8vrlvkb7dqw-nix-shell/lib -L/nix/store/y0pvdv94v1bwlfm6rv4iy78p94fp8jb6-starpu-1.4.7/lib -L/nix/store/y0pvdv94v1bwlfm6rv4iy78p94fp8jb6-starpu-1.4.7/lib

Now let's see an example to improve our derivation and support enabling a configuration option. StarPU can be compiled in order to run in a discrete simulation, but it requires an additionnal dependency which is Simgrid. We will add two things in the parameters:

# starpu.nix
{
  lib # <-- needed later
  fetchzip,
  stdenv,

  # starpu dependencies
  hwloc,
  fftw,
  fftwFloat,
  pkg-config,
  simgrid ? null,

  # Options
  enableSimgrid ? false,
}:

simgrid ? null, is the simgrid dependency itself, but here we use ? to assign a default value null in case we don't want to compile with simgrid. enableSimgrid ? false, is a custom parameter to enable/disable the simgrid mode. By default it is disabled. Then we add the dependency in the body of the derivation:

# ...
stdenv.mkDerivation (finalAttrs: {
  pname = "starpu";
  version = "1.4.7";

  inherit enableSimgrid;

  # ...

  nativeBuildInputs = [
    pkg-config
    hwloc
  ]
  ++ lib.optional finalAttrs.enableSimgrid simgrid;

  buildInputs =
  [
    fftw
    fftwFloat
    hwloc
    pkg-config
  ]
  ++ lib.optional finalAttrs.enableSimgrid simgrid;

  configureFlags = []
  ++ lib.optional finalAttrs.enableSimgrid "--enable-simgrid";

  # ...
})

Final file:

# starpu.nix
{
  lib,
  fetchzip,
  stdenv,

  # starpu dependencies
  hwloc,
  fftw,
  fftwFloat,
  pkg-config,
  simgrid ? null,

  # Options
  enableSimgrid ? false,
}:

stdenv.mkDerivation (finalAttrs: {
  pname = "starpu";
  version = "1.4.7";

  inherit enableSimgrid;

  src = fetchzip {
    url = "https://files.inria.fr/starpu/starpu-${finalAttrs.version}/starpu-${finalAttrs.version}.tar.gz";
    hash = "sha256-6AjQr+2nSJ/uYjJ6II4vJgxj5nHuvlsLvPGZZv/cU8M=";
  };

  nativeBuildInputs = [
    pkg-config
    hwloc
  ]
  ++ lib.optional finalAttrs.enableSimgrid simgrid;

  buildInputs =
  [
    fftw
    fftwFloat
    hwloc
    pkg-config
  ]
  ++ lib.optional finalAttrs.enableSimgrid simgrid;

  configureFlags = []
  ++ lib.optional finalAttrs.enableSimgrid "--enable-simgrid";

  postConfigure = ''
    patchShebangs --build doc/extractHeadline.sh doc/fixLinks.sh
  '';
})

Using the newly created parameter:

# shell.nix
let
  pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.11.tar.gz") { };
  starpu = pkgs.callPackage ./starpu.nix { enableSimgrid = true; }; # <-- using the parameter
in
  pkgs.mkShell {
    packages = [ starpu ];
  }

During the configurePhase we should see: simgrid enabled: yes. Also you may add this in the derivation enableParallelBuilding = true; 😄

< Previous post | Next post >