Special thanks to Utku Demir for review and constant inspiration.
This post was originally published at https://robertwpearce.com/hakyll-pt-6-pure-builds-with-nix.html.
This is part 6 of a multipart series where we will look at getting a website / blog set up with hakyll and customized a fair bit.
- Pt. 1 – Setup & Initial Customization
- Pt. 2 – Generating a Sitemap XML File
- Pt. 3 – Generating RSS and Atom XML Feeds
- Pt. 4 – Copying Static Files For Your Build
- Pt. 5 – Generating Custom Post Filenames From a Title Slug
- Pt. 6 – Pure Builds With Nix
- (wip) Pt. 7 – Customizing Markdown Compiler Options
Overview
In this post we're going to create a new hakyll site from scratch with a caveat: we will do just about everything with nix in order to guarantee reproducibility for anyone (or anything) using our project. There are also two bonuses that we will inherit simply because we are using nix:
- we will not need to rely on global package installs (apart from nix, of course)
- we will be able to easily patch any package problems; for example, if some of hakyll's dependencies are not available in nixpkgs, we can patch hakyll to get it to work.
Here is the example repository with what we're going to make: https://github.com/rpearce/hakyll-nix-example
Note: this post assumes that you have installed nix on your system.
Steps to Build Our Hakyll Project With Nix
Make a new project with release.nix, default.nix, and shell.nix, and get into its pure nix shell environment:
λ mkdir hakyll-nix-example && cd $_
λ echo "{ }: let in { }" > release.nix
λ echo "(import ./release.nix { }).project" > default.nix
λ echo "(import ./release.nix { }).shell" > shell.nix
λ nix-shell --pure -p niv nix cacert
We won't have to touch default.nix nor shell.nix again, for we are delegating their responsibilities to the release.nix file that we'll add more to in a moment.
Note: we require nix and cacert when running a pure nix-shell with niv because of this issue: https://github.com/nmattia/niv/issues/222.
Now that we're in the nix shell, initialize niv and specify your nixpkgs owner, repository, and branch to be whatever you want:
[nix-shell:~/projects/hakyll-nix-example]$ niv init
[nix-shell:~/projects/hakyll-nix-example]$ niv update nixpkgs -o NixOS -r nixpkgs-channels -b nixpkgs-unstable
[nix-shell:~/projects/hakyll-nix-example]$ exit
Update your release.nix file with the following:
let
  sources = import ./nix/sources.nix;
in
{ compiler ? "ghc883"
, pkgs ? import sources.nixpkgs { }
}:
let
  inherit (pkgs.lib.trivial) flip pipe;
  inherit (pkgs.haskell.lib) appendPatch appendConfigureFlags;
  haskellPackages = pkgs.haskell.packages.${compiler}.override {
    overrides = hpNew: hpOld: {
      hakyll =
        pipe
           hpOld.hakyll
           [ (flip appendPatch ./hakyll.patch)
             (flip appendConfigureFlags [ "-f" "watchServer" "-f" "previewServer" ])
           ];
      hakyll-nix-example = hpNew.callCabal2nix "hakyll-nix-example" ./. { };
      niv = import sources.niv { };
    };
  };
  project = haskellPackages.hakyll-nix-example;
in
{
  project = project;
  shell = haskellPackages.shellFor {
    packages = p: with p; [
      project
    ];
    buildInputs = with haskellPackages; [
      ghcid
      hlint       # or ormolu
      niv
      pkgs.cacert # needed for niv
      pkgs.nix    # needed for niv
    ];
    withHoogle = true;
  };
}
Don't worry, we'll circle back to what we just did.
Create a hakyll.patch diff file:
λ touch hakyll.patch
Bootstrap the hakyll project (we won't ever need this again):
λ nix-shell --pure -p haskellPackages.hakyll --run "hakyll-init ."
Build the project and --show-trace just in case something goes wrong:
λ nix-build --show-trace
Run the local dev server:
λ ./result/bin/site watch
Navigate to http://localhost:8000 and see your local dev site up and running!
  
  
  Understanding the release.nix File
Let's break down what we copied and pasted into release.nix.
let
  sources = import ./nix/sources.nix;
in
{ compiler ? "ghc883"
, pkgs ? import sources.nixpkgs { }
}:
# ...
The let gives us the space to define an attribute (variable), and it is here that we import our sources.nix file that was generated by niv. The in block defines a function parameter with two attributes, compiler and sources, that each have defaults (when there's a :, that means what comes next is a function body or another function argument). For the compiler, we will use this version to compile all of the Haskell packages that we interact with. For the pkgs, we default to using our pinned version of nixpkgs, but this is overridable.
# ...
let
  inherit (pkgs.lib.trivial) flip pipe;
  inherit (pkgs.haskell.lib) appendPatch appendConfigureFlags;
  # ...
Our new let falls within the function we created above, and we then state that we would like to inherit some nice functions from pkgs.lib.trivial and pkgs.haskell.lib.  The flip and pipe functions are standards in functional programming, but I'll share a short recap:
- 
fliptakes a functiona -> b -> cand flips the accepted arguments to act likeb -> a -> c. Its definition isflip = f: a: b: f b a;– it takes a function, thena, thenb, and then it appliesaandbin reversed (flipped) order.
- 
pipeestablishes a set of functions that you can apply data to, one after the other. Think of bash pipes:cat blog_post.txt | grep nix.
let
  # ...
  haskellPackages = pkgs.haskell.packages.${compiler}.override {
    overrides = hpNew: hpOld: {
      hakyll =
        pipe
           hpOld.hakyll
           [ (flip appendPatch ./hakyll.patch)
             (flip appendConfigureFlags [ "-f" "watchServer" "-f" "previewServer" ])
           ];
      hakyll-nix-example = hpNew.callCabal2nix "hakyll-nix-example" ./. { };
      niv = import sources.niv { };
    };
  };
  # ...
This uses our pinned (or overridden) nixpkgs to create our own haskellPackages for a specific Haskell compiler version.
For hakyll, we need to make sure it gets compiled with the watchServer and previewServer flags, or we won't be able to use its local dev server. We also provide an optional patch file (git diff > hakyll.patch file) that we can build hakyll with if there are any changes to the project that we need to make.  Patch files can be empty when no patches are required, but if you do need to patch something, here is an example hakyll.patch file:
diff --git a/hakyll.cabal b/hakyll.cabal
index fcded8d..9746f20 100644
--- a/hakyll.cabal
+++ b/hakyll.cabal
@@ -199,7 +199,7 @@ Library
   If flag(previewServer)
     Build-depends:
       wai             >= 3.2   && < 3.3,
-      warp            >= 3.2   && < 3.3,
+      warp,
       wai-app-static  >= 3.1   && < 3.2,
       http-types      >= 0.9   && < 0.13,
       fsnotify        >= 0.2   && < 0.4
The hakyll-nix-example attribute is specifically for our Haskell project in order for us to be sure our project is compiled with our desired compiler version. We leverage the callCabal2nix tool to handle automatically converting our hakyll-nix-example.cabal file into a nix derivation for our build.
Lastly, we ensure that the niv we are using in the nix-shell is our pinned niv that niv itself generated.
let
  # ...
  project = haskellPackages.hakyll-nix-example;
in
{
  project = project;
  # ...
The project attribute is what our default.nix will use when being called with tools like nix-build. All we do is access our hakyll-nix-example attribute from our customized haskellPackages.
let
  # ...
in {
  # ...
  shell = haskellPackages.shellFor {
    packages = p: with p; [
      project
    ];
    buildInputs = with haskellPackages; [
      ghcid
      hlint       # or ormolu
      niv
      pkgs.cacert # needed for niv
      pkgs.nix    # needed for niv
    ];
    withHoogle = true;
  };
}
Exactly like default.nix uses the project attribute, shell.nix is looking for a shell attribute to define everything it needs when running nix-shell --pure. We use shellFor, which comes with the nixpkgs Haskell tools, and we provide it a few attributes:
- the packagesattribute holds ourprojectpackage and any othernixpkgsthat you would like to have built when entering the shell
- the buildInputsattribute holds all the tools that we'll have available to us while we're in the shell; for example, you can runghcidand load your Haskell code to test it out or runhlintto lint your Haskell files
- 
withHooglegives us the ability to query https://hoogle.haskell.org
Wrapping Up
Using nix to build our project helps make development consistent and predictable; however, learning nix is not necessarily a breeze. The following articles directly contributed to my understanding that led to this post:
- https://nixos.org/nixos/nix-pills/index.html
- https://github.com/Gabriel439/haskell-nix/tree/master/project4
- https://turbomack.github.io/posts/2020-02-17-cabal-flags-and-nix.html (this article is what led me to learn how to patch hakyll)
- 
overrideAttrs: https://nixos.org/nixpkgs/manual/#sec-pkg-overrideAttrs
- 
overlays: https://nixos.org/nixpkgs/manual/#chap-overlays
- https://maybevoid.com/posts/2019-01-27-getting-started-haskell-nix.html
- https://kuznero.com/post/linux/haskell-project-structure-in-nixos/
- https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/haskell.section.md
- https://nixos.org/nixpkgs/manual/#haskell
Next up:
- (wip) Pt. 7 – Customizing Markdown Compiler Options
Thank you for reading!
Robert
 

 
    
Top comments (0)