nix blueprints

oops, well this took to long; but nothing has really changed with the website in the last few years, and currently the only posts are triggered by technical changes. maybe we can change that.

# flake.nix
{
  description = "personal website";
  inputs = {
    blueprint.inputs.nixpkgs.follows = "nixpkgs";
    blueprint.url = "github:numtide/blueprint";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  };
  outputs = inputs: inputs.blueprint {inherit inputs;};
}

now, blueprint, being an opinionated framework, insisted that all these files be pure, and specify all their inputs. normally flakes let you play fast and loose with flake input variables, running allover the global flake output scope. however, here all input files need to clarify what is coming into them. furthermore, any library functions (those that live in lib/ and are accessible via lib.<attr>) have no access default access to the automagic pkgs [recte nixpkgs] input attr. this means we needed to do a bit of a functional programming style, expose functions that pick the packages we want, instead of just exposing the list of packages we want. but lo, we now only specify package dependencies once, and get them everywhere!

# lib/default.nix
_: {
  checkInputs = pkgs: with pkgs; [alejandra statix prettier];
  nativeBuildInputs = pkgs: with pkgs; [gnutar hugo];
  runtimeInputs = pkgs: with pkgs; [hut];
}
# packages/deploy.nix
{pname, pkgs, flake, perSystem, ...}:
pkgs.writeShellApplication {
  name = pname;
  runtimeInputs = flake.lib.runtimeInputs pkgs;
  text = ''
    hut pages publish ${perSystem.self.default} \
      --domain www.urandom.co.uk \
      --site-config ${builtins.toFile "config.json" (builtins.toJSON {notFound = "/404.html";})}
  '';
}
# formatter.nix
{pkgs, flake, ...}:
pkgs.writeShellApplication {
  name = "fmt";
  runtimeInputs = flake.lib.checkInputs pkgs;
  text = ''
    alejandra --quiet .
    statix fix .
    prettier --log-level error --write .
  '';
}
# devshell.nix
{pkgs, flake, perSystem, ...} @ inputs: pkgs.mkShell {
  packages = with flake.lib; checkInputs pkgs ++ nativeBuildInputs inputs ++ runtimeInputs pkgs;
}

we also could not help ourselves from trying to make our checks more dry, with a bespoke lib.mkCheck function:

# lib/default.nix
{flake, ...}: {
  # ...
  mkCheck = {pname, pkgs, checkPhase}:
    pkgs.stdenv.mkDerivation {
      name = pname;
      src = flake;
      nativeBuildInputs = flake.lib.checkInputs pkgs;
      dontBuild = true;
      doCheck = true;
      inherit checkPhase;
      installPhase = "touch $out";
    };
  # ...
}
# checks/alejandra.nix
{pname, flake, pkgs, ...}:
flake.lib.mkCheck {
  inherit pname pkgs;
  checkPhase = "${pname} --check --quiet .";
}

as part of not updating the repo in four years, we did end up pinning hugo to 0.125.0; something that was deeply existentially painful, and technically trivial: thank nix. it involved adding a new flake input and plumbing it into packages.default via lib.nativeBuildInputs.

# flake.nix
{
  # ...
  inputs.hugo.url = "github:nixos/nixpkgs/080a4a27f206d07724b88da096e27ef63401a504";
  # ...
}

unfortunately, we did have to abandon the stupid simple pkgs input for a more complex, fish out the inputs we want from the flake, approach; but blueprint make this pretty straightforward too.

# lib/default.nix
_: {
  # ...
  nativeBuildInputs = {flake, perSystem, ...}:
    with perSystem; [nixpkgs.gnutar hugo.hugo];
  # ...
}
# package/default.nix
{pkgs, flake, perSystem, ...} @ inputs: pkgs.stdenv.mkDerivation {
  # ...
  nativeBuildInputs = flake.lib.nativeBuildInputs inputs;
  # ...
}

one drawback is the direnv integration only looks for changes in flake.nix when deciding if to recompute the shell environment. usually this should change slowly, it does also look at flake.nix, and a simple touch flake.nix will fix it, but it is a regression.

See also