Cabal Flags With and Without Nix Hakyll build environment with watcher and preview server using nix

Posted in: haskell, nix.
Posted on February 17, 2020

Yak shaving for every day. I wanted to write post about something completely different but I needed to shave a beast and figured I can as well share “a quick how to” in hope I might safe someone else’s time.

I’ve been using NixOS exclusively on basically all of my machines. I also like to abuse nix for all sort of stuff - for creating project environments using nix-shell, deployment etc. Anyway as it is pretty common at least in my case, the software gods were pretty angry with me again. Even though I have this site “nixified” for over a year it turned out Hakyll in nixpkgs is not built with previewServer and watchServer which I would pretty much like to use.

Discussion under issue 442 in cabal2nix shed a bit more light onto the issues described within this post. The issue with

  1. There are good reasons why doJailbreak ignores conditional dependencies. See this comment for more informations.
  2. The reason why Hakyll in nixpkgs is not built with default previewServer and watchServer is due to Cabal Install. Nixpkgs set doesn’t contain compatible version of warp package so Hakyll can’t be built with those flags. In situations like this Cabal simply silently ignores the flag. This behaviour seems at best odd to me. There is currently some dicussion about making flags explicit. Change like this might make maintenance of nixpkgs a bit more difficult so we will see how this goes.

Cabal Flags

Lets start by familiarizing ourselves with Cabal Flags first. Cabal allows user to define custom build configuration flags within cabal file. Declaration of custom flags is usually in top section of Cabal file. Flag has to be Bool value and can be both True or False by default.

Looking into hakyll.cabal we can see two flags watchServer and previewServer. Interestingly enough, both are set to True by default. This is can configure to overwrite default flags:

$ cabal configure -f watchServer -f previewServer

Frankly it was more difficult for me to found this information than I think it should have been. It’s hard to point a finger to why so. Maybe it’s hard to navigate cabal documentation, maybe the error message Hakyll logs when it’s missing a flag is not helpful enough or maybe it was purely my own stupidity. Anyway Hackage lists available flags on package description page including a link to relevant part of documentation of cabal so it’s not like folks didn’t try to be helpful.

Haskell Infrastructure for Nix

Like Stack, nixpkgs provide curated Haskell package sets but with both flexibility and user experience (meaning complexity) of Nix. Cabal2Nix is a standard tool for generating Nix expressions for Cabal projects.

$ cabal2nix . > site.nix

If you don’t want to pre-generate nix expressions you can as well use haskellPackages.cabal2nix "site" ./. as a part of build rather than generating it upfront.

There are two gotchas though. First Hakyll within nix packages is build with watchServer and previewServer disabled so these features are not available, second the standard jailbreak1 seems not to be compatible with cabal flags.

Overriding Hakyll

To fix these two problems we’re going to override the definition of Hakyll. For general structure of default.nix file I’m roughly following Gabriel’s haskell-nix way which looks like this:

let
  config = {
    packageOverrides = pkgs: {
      haskellPackages = pkgs.haskellPackages.override {
        overrides = haskellPackagesNew: haskellPackagesOld: {
          site = haskellPackagesNew.callPackage ./site.nix {};
        };
      };
    };
  };
  pkgs = import <nixpkgs> { inherit config; };
in pkgs.haskellPackages.site;

This will extend default package set with my custom site package.

Next thing we need to do is to add overrides for Hakyll. We’re going to use version already available to us in haskellPackages and just apply desired changes. For start we need to configure projects with watchServer and previewServer flags:

#...
      haskellPackages = pkgs.haskellPackages.override {
        overrides = haskellPackagesNew: haskellPackagesOld: {
          hakyll = haskellPackagesOld.hakyll.overrideAttrs(old: {
            configureFlags = "-f watchServer -f previewServer"; # pass configure flags
            jailbreak = true; # jailbreak dependecies
          });
          site = haskellPackagesNew.callPackage ./site.nix {};
        };
      };
#...

This is as far as I know not documented but it’s easy enough to reverse engineer from the generir-builder.nix.

At this stage build fails for me though because of unavailable version of warp. I assume this is because jailbreak doesn’t work correctly with flag directive.

To solve this problem I’m going to define custom patch for Hakyll. I usually use git to produce patches.

  1. git clone the repository
  2. do desired changes to source locally
  3. run git diff > file.patch to produce patch file

Patch file I’m using.

The simplest way to apply custom patches in nix expression is to use patches attribute as follows:

#...
      haskellPackages = pkgs.haskellPackages.override {
        overrides = haskellPackagesNew: haskellPackagesOld: {
          hakyll = haskellPackagesOld.hakyll.overrideAttrs(old: {
            configureFlags = "-f watchServer -f previewServer";
            patches = [./hakyll.patch]; # applying our custom patch
          });
          site = haskellPackagesNew.callPackage ./site.nix {};
        };
      };
#...

Now we have build for our site binary using the customized version of Hakyll with support for watch command.

Conclusion

More than anything this demonstrates how frustrating it sometimes can be to use software. I wanted to blog about something but ended up fixing build and then writing this tutorial instead. A big part of using software is about putting through with things like this. There is always some trade-off with tools and approaches we use. For me personally it’s important above all to be able to go and fix issues myself without going completely crazy in the process. Both Haskell and Nix are good tools for this. On the other hand neither of them is the most effective way of avoiding problems like in first place.

I’m not going to rant about the state of the software. Neither I’m going to praise tools I use. I’m writing this post with two goals:

  1. As a mostly copy paste tutorial for folks who might run into similar issues.
  2. As an exercise for reader to see both power and cost of getting things done with tools like these.

Whole code can be found on GitHub


  1. jailbreak attribute or doJailbreak function are used to remove version constrains from cabal file so package can be build with other version of dependencies present in used haskellPackages set.↩︎