____ _ | __ )| | ___ __ _ | _ \| |/ _ \ / _` | | |_) | | (_) | (_| | |____/|_|\___/ \__, | |___/_ _ _/_| |__ ___ _ __| |_ _ _/_ / _ \ '_ \ / _ \ '__| | | | |/ _ \ __/ |_) | __/ | | | |_| | __/ \___|_.__/ \___|_| |_|\__,_|\___|
[2025-03-07][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-, so we replace
with ${finalAttrs.version} so the value of the version attribute
defined above can be evaluated.
Remarks:
version via finalAttrs because our structure is wrapped
into mkDerivation ( finalAttrs: {} ). Another way would be to use a recursive set like
this: mkDerivation rec {}, thus every attributes can be accessed recursively within the
set.Alright, now let's try to run derivation. We use nix-shell -E 'with import 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.
[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.
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!
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/. 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";
# ...
})
inherit enableSimgrid; gets enableSimgrid variable and put it in the
finalAttrs set under the same name.++ lib.optional finalAttrs.enableSimgrid simgrid; tests if enableSimgrid
is true, then add simgrid to the list of buildInputs/nativeBuildInputs../configure. Here when simgrid is enable it will add "--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; 😄