NixOS, again!

Now I promise this isn’t just a NixOS advocacy site; I do have some other content planned, honest! I just wanted to give a couple of examples of the using Nix as part of your daily workflow of building stuff.

In something like Debian, you may use .deb files/packages often, but it’s normally without thinking about it too much, and you certainly don’t create a deb just to build a document, for example. But with Nix, because all the time you’re building these little functions to build something in a fairly pure, referentially-transparent way, you can also reuse them and combine them in new ways, which can be quite neat.

So, previously, I’ve shown a slightly esoteric example of setting up LaTeX in Nix. We ended up with a file ~/.dotfiles/myfonts.nix which can build a number of things, including a TexLive derivation with some custom fonts added to it. Typically, when working in LaTeX, I’ll create a Makefile which contains something like this:

cv.pdf: cv.tex $(shell find . -type f -name *.svg)
pdflatex -shell-escape cv && pdflatex -shell-escape cv && pdflatex -shell-escape cv

clean:
- rm *.aux *.bbl *.blg *.log *.ptb cv.pdf

(I’m currently updating my CV, hence this example.) This approach, with a Makefile, works just fine. But, it does rely a lot on finding its dependencies in the current environment - it’s not referentially-transparent by any stretch of the imagination. But if we use Nix, we can take advantage of the work we’ve done previously and make it all a little purer. So in the same directory, we’ll add a default.nix with this:

{ pkgs ? import <nixpkgs> {} }:

let
  fonts = import /home/matthew/.dotfiles/myfonts.nix { inherit pkgs; };
in
pkgs.runCommandLocal "cv.pdf" {
  src = ./cv.tex;
  buildInputs = [ fonts.mytexlive ];
} ''
  pdflatex -shell-escape $src && pdflatex -shell-escape $src && pdflatex -shell-escape $src
  cp $(basename $src .tex).pdf $out
''

We import the file we’ve made previously, and then it’s just a single call to the pkgs.runCommandLocal function, providing a reasonably arbitrary name, the environment in which we declare the src to be the local tex file, and buildInputs to be our custom TexLive derivation, and then the shell commands for what to do.

Now, instead of running make, we can run nix-build. In fact, we can run okular $(nix-build) because nix-build puts all logs over stderr and the only thing that comes out of stdout is the $out path created in the nix-store, which is the path of the PDF itself. Is this really any better than running make? No, not massively. It’s a little cute, and it means that I can take mytexlive out of my ~/.config/nixpkgs/home.nix if I want to, but this isn’t exactly going to save me lots of time. It makes things a little tidier.

Hugo, direnv etc

This website is generated by Hugo. I’ve only just started using it but it seems quite nice. hugo behaves for me and hasn’t required a lot of configuring. There are already a couple of blog posts out there for using hugo with Nix. They seem fairly similar. They use direnv which I’ve not used before and seems interesting: the concept is that you can get scripts to run when you cd into a directory. In Nix, we can harness this to add programs (e.g. hugo) to our profile, but only when we’re working within a directory. Which seems nice. I’ve gone a bit further than those existing blog posts though, in order to get generating the whole site being driven through Nix too.

Setting up direnv

Home-manager supports direnv, so in ~/.config/nixpkgs/home.nix I have added:

programs.direnv = {
  enable = true;
  enableBashIntegration = true;
  nix-direnv.enable = true;
};

Now run home-manager switch as normal. I’m going to be working in ~/websites/wellquite/ from here on:

# mkdir -p ~/websites/wellquite && cd ~/websites/wellquite
# echo "use nix" >> .envrc
# direnv allow

Open a new terminal and cd ~/websites/wellquite. You need a new terminal / shell so that it runs the direnv shell hooks.

Generating the website

Now, I want to use pkgs.mkShell to add hugo and some theme setup stuff to my profile whenever direnv activates itself. But, I also want to be able to run nix-build in here and essentially have it run hugo in a clean state and generate the entire site. Direnv will look for a shell.nix file and run that. nix-build uses default.nix by default. So the plan is to declare as much as possible in default.nix, and then shell.nix will import that, and make minor further tweaks.

So, default.nix:

{ pkgs ? import <nixpkgs> {}}:

let
  hugo-theme-indigo = pkgs.stdenvNoCC.mkDerivation {
    name = "hugo-theme-indigo";
    src = pkgs.fetchFromGitHub {
      owner = "AngeloStavrow";
      repo = "indigo";
      rev = "7cfe70c0014d6162b81e40358df67c566bbe0296";
      sha256 = "15mj9yfyfl69kn5myspzcljg2msd5mlfj4dz351b1608icba4g8r";
    };
    installPhase = ''
      cp -r $src $out
    '';
    preferLocalBuild = true;
  };

  wellquite = pkgs.stdenvNoCC.mkDerivation {
    name = "wellquite";

    src = with builtins; filterSource
      (path: type: substring 0 1 (baseNameOf path) != "." && (baseNameOf path) != "default.nix" && (baseNameOf path) != "shell.nix" && type != "symlink")
      ./.;

    dontConfigure = true;
    buildInputs = [ pkgs.hugo hugo-theme-indigo ];
    preferLocalBuild = true;
    installPhase = ''
      runHook preInstall

      mkdir -p themes
      ln -snf "${hugo-theme-indigo}" themes/hugo-theme-indigo
      hugo -d $out

      if [ -d ./overlays ]; then
        cp -R ./overlays/* $out/
      fi

      runHook postInstall
    '';
  };
in
{
  inherit hugo-theme-indigo wellquite;
  inherit (pkgs) hugo;
}

Two things going on here:

  1. hugo-theme-indigo is just how to fetch a theme I’m using from Github. It’s properly pinned so I know it’ll never change without my say-so.
  2. wellquite then uses that theme, and hugo itself, to generate the whole site.

In wellquite, .src looks a little elaborate. The more obvious thing to do is just src = ./;. If you do that, then the directory’s content will change whenever, say, a result symlink changes, which will happen whenever you run nix-build. You probably don’t want to provide the previous build output as a source to the current build. So there’s a bit of careful filtering going on there to make sure that no dotfiles, no symlinks, and no default.nix or shell.nix get treated as sources. This just ensures that repeatedly calling nix-build can spot if no real changes have happened to our source, in which case there may be nothing to (re)build.

I have some other plain HTML files sitting in ./overlays which I wish to just have copied into the output, hence those extra few lines of shell. With all this, I can now call nix-build -A wellquite and I should then find the whole site built in ./result, which I can then rsync up to my server.

Hooking into the shell

With the bulk of the work now done, I just want a small shell.nix which can reuse a bunch of things from default.nix and add hugo to my user profile so that I can do handy things like run hugo serve when writing articles. Hence, shell.nix:

{ pkgs ? import <nixpkgs> {}}:

let
  myhugo = import ./default.nix { inherit pkgs; };

in pkgs.mkShell {
  buildInputs = [ myhugo.hugo myhugo.hugo-theme-indigo ];
  shellHook = ''
    mkdir -p themes
    ln -snf "${myhugo.hugo-theme-indigo}" themes/hugo-theme-indigo
  '';
}

Now, whenever I cd ~/websites/wellquite, the shell.nix will be run. It’ll import from default.nix thus reusing code we’ve already written, and then just add a symlink so that my chosen theme is available locally. I can then run hugo serve and it all just works.

The ability to easily reuse things you’ve previously built is quite neat, and it’s just much better done than having a whole load of shell scripts lying around all over the place. It’s not going to radically change my life, but it does feel a little more robust and defensive, which I like.