Hakyll Pt. 6 – Pure Builds With Nix
Robert Pearce
Posted on April 30, 2020
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:
-
flip
takes a functiona -> b -> c
and 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 appliesa
andb
in reversed (flipped) order. -
pipe
establishes 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
packages
attribute holds ourproject
package and any othernixpkgs
that you would like to have built when entering the shell - the
buildInputs
attribute holds all the tools that we'll have available to us while we're in the shell; for example, you can runghcid
and load your Haskell code to test it out or runhlint
to lint your Haskell files -
withHoogle
gives 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
Posted on April 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.