../nix-flake-for-static-blog

Nix Flake for a static blog

Flakes

Nix allows you to define exactly how a particular program is build, tested and installed. The end result is what's called a derivation, which explicitly defines inputs (what does this program depend on), and outputs (the binary for example).

The inputs can be at build or runtime. For example in order to statically compile a binary that uses libyaml you can specify that it needs libyaml at build time but not at runtime.

Flakes are a Nix feature that allows you to do that while pinning the input versions.

In this short post we are gonna define a flake for my static blog.

Our Flake

A flake has three main top level attributes

{
  description = "Flake description";
  inputs = { ... };
  outputs = { ... };
}

The description is fairly self-explanatory, inputs is a set of inputs that this flake will take, everything we use needs to be in this set, there are no globals, no implicit packages, etc. outputs defines what this flake provides.

Inputs

Let's see how inputs are defined, when working in non-flake Nix we usually have <nixpkgs> imported by default, so we have access to the whole package repository of nix. In flakes we have to set nixpkgs explicitly as an input if we need to.

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  };
}

I also import a couple of helpers that help us write flakes.

inputs = {
  nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  flake-parts.url = "github:hercules-ci/flake-parts";
  systems.url = "github:nix-systems/default";
}

When nix sees this, it goes to those repositories and looks for a flake.nix file. For example here is the one for flake-parts. You should mostly understand what is going on there at least at a high level. Inputs and outputs.

Everything expose in those flakes will be available in outputs under inputs.<input-name>.*.

Outputs

We have a single output for our blog, the rendered blog. In my case my blog is rendered using Zola.

Let's start by writing the nix derivation that builds the blog, this is flake-agnostic. Then later we are gonna integrate it into the flake.

Derivation

A nix derivation should be pretty familiar to most software engineers, you explicityly state dependencies (in different attributes, depending on when the dependency is needed), and the build steps.

Let's see the derivation with some clarifying comments:

pkgs.stdenv.mkDerivation {
  name = "dziban-blog";
  
  # depsBuildBuild are used only at build time, the resulting
  # derivation has no dependency on it.
  depsBuildBuild = [ pkgs.zola ];
  
  # Specify where to get the source, this will be copied to the
  # $src directory inside the build sandbox.
  src = ./.;
  
  # Shell script that builds whatever is needed. It has
  # depsBuildBuild dependencies available
  buildPhase = ''
  zola build
  '';
  
  # Shell script that installs whatever is needed.   
  # We have an envvar $out that is the directory where
  # nix will expect all the results of building the derivation
  installPhase = ''
  mkdir -p $out
  mv public/* $out
  '';
}

It's fairly straightforward, we define that zola is a dependency at build time, specify that the source is everything in the current directory, then we build and install.

The whole output expression

Now let's add this to the flake outputs, we use a flake-parts library called mkFlake that helps us in several ways that I won't go now, you can check their website for more info.

But basically you give it the systems this flake is available in (for example x86_64-linux) and then define what outputs you have per system.

{
  outputs = inputs: 
    inputs.flake-parts.lib.mkFlake { inherit inputs; } {
      systems = import inputs.systems;
      perSystem = { config, self', pkgs, ... }: {
        packages = {
          dziban-blog = pkgs.stdenv.mkDerivation { ... };
          default = self'.packages.dziban-blog;
        };
      };
    };
}

Instead of hardcoding the systems we added the systems flake to do that, we pass it to the systems attribute of mkFlake. Then perSystem runs for each system we defined and in this case defines only packages outputs.

packages.dziban-blog is the derivation we wrote above. default is the package that nix looks for when one is not specified when building or installing, we just point it to the dziban-blog package.

Building

To build a flake you just run nix build. Nix will gather the inputs, create a flake.lock with the pinned versions of dependencies and execute the nix expression. In this case that means building the derivation we defined.

The $out directory is then symbolically linked in the current directory as ./result and it contains the whole content of public/ after building.

[marcecoll@nixos:~/proj/dziban]$ ls -l ./result
lrwxrwxrwx 1 marcecoll users 55 abr 23 13:13 ./result -> /nix/store/512fd9y2l9xs1m5vgd2i7wsdchr8pb4q-dziban-blog

[marcecoll@nixos:~/proj/dziban]$ ls ./result
404.html  blog                essays                   images      posts       search_index.en.js  style.css
atom.xml  elasticlunr.min.js  exploratory-programming  index.html  robots.txt  sitemap.xml

We can also inspect the resulting derivation, the result is very long but I'm gonna highlight some parts of it

[marcecoll@nixos:~/proj/dziban]$ nix derivation show ./result

{
    "name": "dziban-blog",
    "outputs": {
      "out": {
        "path": "/nix/store/512fd9y2l9xs1m5vgd2i7wsdchr8pb4q-dziban-blog"
      }
    },
    "system": "x86_64-linux"

The name and current system plus the output path. You can see it's the same that ./result is linked to.

   "inputDrvs": {
      "/nix/store/96j7mvs5427kbfbg02zbf3586gnsr7b6-stdenv-linux.drv": {
        "dynamicOutputs": {},
        "outputs": [
          "out"
        ]
      },
      "/nix/store/jnm2249gyarbs1nir7mzam257fgamrpf-bash-5.2p26.drv": {
        "dynamicOutputs": {},
        "outputs": [
          "out"
        ]
      },
      "/nix/store/yvadchlyfi4m9xwz8bvw7mcnyys30z0j-zola-0.18.0.drv": {
        "dynamicOutputs": {},
        "outputs": [
          "out"
        ]
      }
    },

These are the inputs to the derivation, bash is needed to run the steps, stdenv contains the mkDerivation code and zola is used to build the blog.

The Whole Flake

{
  description = "blog.dziban.net";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
    systems.url = "github:nix-systems/default";
  };

  outputs = inputs: 
    inputs.flake-parts.lib.mkFlake { inherit inputs; } {
      systems = import inputs.systems;
      perSystem = { config, self', pkgs, ... }: {
        packages = {
	        dziban-blog = pkgs.stdenv.mkDerivation {
	          name = "dziban-blog";
	          depsBuildBuild = [ pkgs.zola ];
	          src = ./.;

	          buildPhase = ''
            	zola build
	          '';

	          installPhase = ''
	            mkdir -p $out
	            mv public/* $out/
	          '';
	        };

	        default = self'.packages.dziban-blog;
	      };
      };
    };
}

In a future post I'm gonna talk about how to take this flake and easily deploy it on a NixOS server.