Asko Soukka

Stateless Nix environments revisited

It’s almost a year, since I tried to bend Nix package manager to fit my own workflows for the first time. I disliked the recommended way of describing nix environments in global configuration and activating and deactivating them in statefull way. Back then, I worked my way around by defining a wrapper to make local nix-expressions callable executables.

Consider that deprecated.

Nix 1.9 introduced shebang support to use Nix-built interpreter in callable scripts. This alone is a major new feature and solves most of my use cases, where I wanted to define required Nix-dependencies locally, as close to their usage as possible.

Still, I do have a one more use case: For example, want to run make with an environment, which has locally defined Nix-built dependencies. Because the make in this particular example results just a static PDF file, it does not make sense to make that project into a Nix derivation itself. (Neither does it make much sense to make Makefile an executable.)

Of course, I start with defining my dependencies into a Nix derivation, and to make that more convenient with nix-shell, I save that into a file called ./default.nix:

with import <nixpkgs> {}; {
  myEnv = stdenv.mkDerivation {
    name = "myEnv";
    buildInputs = [
      (texLiveAggregationFun { paths = [
        texLive
        texLiveAuctex
        texLiveExtra
        texLivePGF
      ];})
      (rWrapper.override { packages = with rPackages; [
        tikzDevice
      ];})
      dot2tex
      gnumake
      graphviz
      pythonPackages.dateutil
      pythonPackages.matplotlib
      pythonPackages.numpy
      pythonPackages.scipy
    ];
  };
}

Now, I can run make in that environment in a stateless manner with:

$ nix-shell --pure --run "make clean all"

Unfortunately, while that works, it’s a bit long command to type every time.

Initially, I would have preferred to be able to define local callable script named ./make, which was possible with my old approach. Yet, this time I realized, that I can reach almost the same result by defining the following bash function to help:

function nix() { echo nix-shell --pure --run \"$@\" | sh; }

or with a garbage collection root to avoid re-evaluation the expression on every call:

function nix() {
    if [ ! -e shell.drv ]; then
        nix-instantiate --indirect --add-root $PWD/shell.drv
    fi
    echo nix-shell $PWD/shell.drv --pure --run \"$@\" | sh;
}

With this helper in place, I can run any command from the locally defined default.nix with simply:

$ nix make clean all