My experience with NixOS

Posted on June 28, 2014 by Fūzetsu

This post is for Haskellers interested in nix (the package manager) and maybe even NixOS (a distribution built on nix). If you’re not interested then skip it, but I know many people are. It describes how I made the switch and some of my experiences since. I have put off this blog post for a long time, hoping to write it up once I have everything working just like I want it but I was finally motivated to write it up by people expressing interest on IRC. I know many people want to switch but aren’t quite there yet, hopefully this can help them make the decision. If you’re interested in nix but not NixOS, you probably want to just skim the beginning.

Please note that things contained here are just my opinions and I’m not some NixOS guru so things stated here may well be inaccurate.

A couple of weeks ago I have switched to NixOS. Like many, I have seen the blogpost by ocharles and have since thought ‘It’d be great to switch but I’d hate to put in the effort’ but the thought crept in. I ask that you read that blog post first. I have even started to set up NixOS on a separate hard-drive. Recently I have finally decided to retire my trusty ThinkPad X61s on which I did my hacking for the past three years: it was overheating, had holes through it (don’t ask), falling apart and I have took it apart so many times that it’s a miracle it even stayed together. This was a perfect chance. I have taken out the SSD (which cost me more than the netbook itself) and repurposed one of my fileservers which was running Gentoo into a desktop machine.

Probably the most vital resource when making the switch is the NixOS is the NixOS manual. I’ll not go over the installation process but you can find my configuration file here.

My current set-up is XMonad without a DE, using SLiM as a log-in DM.

At the beginning I struggled. I had problems understanding how things worked and some software I wanted to use was simply not packaged. I spent the first couple of weeks with KDE and without some software I wanted. This is a bit of a downside: the number of packages is not the greatest of all distributions. Please don’t get me wrong, there is a lot of software already but the chances are that if you’re using something not that common, you might have to package it yourself. The upside is, it’s easy to do.

I will briefly describe some things which will related to Haskell development later. There is a thing called Hydra, it’s a build-bot that NixOS uses. There is a thing called nixpkgs, it is a repository of packages used by NixOS and also nix itself if you aren’t going for the full OS. nixpkgs is essentially a big repository of nix expressions. Hydra looks at this and builds the expressions, resulting in packages. Main re-distribution works in channels: a user subscribes to a channel and when we ask for some package to be installed, this is where the information is taken from. Official channels are effectively nixpkgs at some commit: nixos channel might be a few weeks behind nixpkgs HEAD, nixos-unstable is usually a few days. Channels are updated when Hydra finishes to build a particular jobset: this means you get binaries for the default settings of all Hydra-built packages. This includes Haskell packages!

I will now describe how I have been doing Haskell development. Again, note that this is constantly evolving while I discover new things.

Haskell development with nix/NixOS

Firstly, NixOS is not necessary to benefit. Pretty much everything I say here is due to nix itself.

Perhaps the main motivation for using nix is wanting to avoid cabal hell. The presence of cabal sandboxes and freezing of dependencies has allowed many people to avoid the problem. I myself used sandboxes very soon after they came out and use cabal-dev before that. My main problem with sandboxes is managing them: are you sandboxing a new project? Come back in an hour when text, hxt, lens, attoparsec, haskell-src-exts and whatever else you happen to be using have compiled for the 50th time on your machine. Sure, one can use shared sandboxes but it is a massive pain. I have wasted hours of my life recompiling same dependencies. nix allows you to avoid this.

I will consider a few scenarios and any potential problems that might come up and how I have dealt (or not dealt!) with them so far.

You have your project. Perhaps the first thing you do is write the cabal file or maybe you already have one but you want to use nix. When we develop, we often want to actually be able to be in the environment of the package, be able to run ghci and all that jazz. There’s a tool called nix-shell which can help you. This effectivelly allows you drop into a sandbox of your project. This is the magical thing ocharles refered to in his blog post. What he did not mention is that you can generate on of these expressions necessary to use nix-shell. Here’s a real example:

[shana@lenalee:/tmp]$ cat Yukari.cabal
name:                Yukari
version:             0.1.0.0
synopsis:            Command line program that allows for automation of various tasks on the AnimeBytes private tracker website.
homepage:            http://github.com/Fuuzetsu/yukari
license:             GPL-3

license-file:        LICENSE

author:              Mateusz Kowalczyk
maintainer:          fuuzetsu@fuuzetsu.co.uk
category:            Utils
build-type:          Simple
cabal-version:       >=1.8

executable yukari
  main-is:             src/Main.hs
  build-depends:       base ==4.*, Yukari

library
  default-language:     Haskell2010

  build-depends:       base ==4.*, curl ==1.3.*, HTTP ==4000.*, filepath ==1.3.*
                       , directory ==1.2.*, bytestring ==0.10.*, network ==2.5.*
                       , text ==1.1.1.*, attoparsec ==0.12.*, HandsomeSoup ==0.3.*
                       , hxt ==9.*, download-curl ==0.1.*, dyre

  hs-source-dirs:       src
  exposed-modules:
    Utils.Yukari
    Utils.Yukari.Crawler
    Utils.Yukari.Filters
    Utils.Yukari.Formatter
    Utils.Yukari.Parser
    Utils.Yukari.Settings
    Utils.Yukari.Spender
    Utils.Yukari.Types

test-suite spec
  type:             exitcode-stdio-1.0
  default-language: Haskell2010
  main-is:          Spec.hs
  hs-source-dirs:
      test

  build-depends:       base ==4.*, Yukari, hspec, QuickCheck == 2.*,
                       filepath==1.3.*, directory ==1.2.*

Then with little help of cabal2nix (the dummy sha256 parameter is a hack here as we’re generating an expression for a source repository).:

[shana@lenalee:/tmp]$ cabal2nix Yukari.cabal --sha256 foo
{ cabal, attoparsec, curl, downloadCurl, dyre, filepath
, HandsomeSoup, hspec, HTTP, hxt, network, QuickCheck, text
}:

cabal.mkDerivation (self: {
  pname = "Yukari";
  version = "0.1.0.0";
  sha256 = "foo";
  isLibrary = true;
  isExecutable = true;
  buildDepends = [
    attoparsec curl downloadCurl dyre filepath HandsomeSoup HTTP hxt
    network text
  ];
  testDepends = [ filepath hspec QuickCheck ];
  meta = {
    homepage = "http://github.com/Fuuzetsu/yukari";
    description = "Command line program that allows for automation of various tasks on the AnimeBytes private tracker website";
    license = self.stdenv.lib.licenses.gpl3;
    platforms = self.ghc.meta.platforms;
  };
})

Note that cabal2nix generates expressions suitable for nixpkgs. To use it for a shell environment, I ammend the resulting expression into following:

{ pkgs ? (import <nixpkgs> {})
, haskellPackages ? pkgs.haskellPackages_ghc763
}:

haskellPackages.cabal.mkDerivation (self: {
  pname = "Yukari";
  version = "0.1.0.0";
  src = /home/shana/programming/yukari;
  isLibrary = true;
  isExecutable = true;
  buildDepends = with haskellPackages; [
    attoparsec curl downloadCurl dyre filepath HandsomeSoup HTTP hxt
    network text
  ];
  testDepends = with haskellPackages; [ filepath hspec QuickCheck ];
  meta = {
    homepage = "http://github.com/Fuuzetsu/yukari";
    description = "Command line program that allows for automation of various tasks on the AnimeBytes private tracker website";
    license = self.stdenv.lib.licenses.gpl3;
    platforms = self.ghc.meta.platforms;
  };
})

If at any point I want to use a different compiler version, I only have to change it at the top (or use a flag to nix-shell) and it will automagically all just work. Now I can use this sandbox:

[shana@lenalee:~/programming/yukari]$ nix-shell --pure

[nix-shell:~/programming/yukari]$ cat .ghci
:set -isrc -fbreak-on-error
[nix-shell:~/programming/yukari]$ ghci
GHCi, version 7.6.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
package flags have changed, resetting and loading new packages...
Loading package array-0.4.0.1 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package containers-0.5.0.0 ... linking ... done.
Loading package filepath-1.3.0.1 ... linking ... done.
Loading package old-locale-1.0.0.5 ... linking ... done.
Loading package time-1.4.0.1 ... linking ... done.
Loading package bytestring-0.10.0.2 ... linking ... done.
Loading package unix-2.6.0.1 ... linking ... done.
Loading package directory-1.2.0.1 ... linking ... done.
Loading package old-time-1.1.0.1 ... linking ... done.
Loading package pretty-1.1.1.0 ... linking ... done.
Loading package process-1.1.0.2 ... linking ... done.
Loading package Cabal-1.16.0 ... linking ... done.
Loading package binary-0.5.1.1 ... linking ... done.
Loading package bin-package-db-0.0.0.0 ... linking ... done.
Loading package hoopl-3.9.0.0 ... linking ... done.
Loading package hpc-0.6.0.0 ... linking ... done.
Loading package template-haskell ... linking ... done.
Loading package ghc-7.6.3 ... linking ... done.
Prelude> :l  Utils.Yukari
[1 of 7] Compiling Utils.Yukari.Types ( src/Utils/Yukari/Types.hs, interpreted )
[2 of 7] Compiling Utils.Yukari.Settings ( src/Utils/Yukari/Settings.hs, interpreted )
[3 of 7] Compiling Utils.Yukari.Parser ( src/Utils/Yukari/Parser.hs, interpreted )
[4 of 7] Compiling Utils.Yukari.Formatter ( src/Utils/Yukari/Formatter.hs, interpreted )
[5 of 7] Compiling Utils.Yukari.Crawler ( src/Utils/Yukari/Crawler.hs, interpreted )
[6 of 7] Compiling Utils.Yukari.Spender ( src/Utils/Yukari/Spender.hs, interpreted )
[7 of 7] Compiling Utils.Yukari     ( src/Utils/Yukari.hs, interpreted )
Ok, modules loaded: Utils.Yukari, Utils.Yukari.Crawler, Utils.Yukari.Formatter, Utils.Yukari.Settings, Utils.Yukari.Spender, Utils.Yukari.Parser, Utils.Yukari.Types.

The --pure stops any ‘globally’ installed tools or packages from polluting the environment which ensures that we only use what we say we do: no surprises because other developer didn’t have ‘somespecialprogram’ installed! Personally I currently use emacs with haskell-mode and I want a REPL in emacs. nix-shell lets you do this. The way I do it is to eval (setq haskell-program-name "nix-repl --pure --command "ghci").

So we managed to sandbox a single project. Cool, but what about if we want to depend on another project? It’s often the case that our project depends on another of our projects which might not be on Hackage or we want to work against dev version or ….

I do this with Haddock: we recently split out haddock parser into a sub-library, ‘haddock-library’. I simply wrote an expression for haddock-library and then import it from haddock expression. Simple:

[shana@lenalee:~/programming/haddock]$ cat default.nix
{ haskellPackages ? (import <nixpkgs> {}).myHaskellPackages_ghcHEAD
, haddockLibrary ? (import /home/shana/programming/haddock/haddock-library
    { haskellPackages = haskellPackages; })
}:

haskellPackages.cabal.mkDerivation (self: {
  pname = "haddock";
  version = "2.15.0";
  src = /home/shana/programming/haddock;
  buildDepends = with haskellPackages;
                   [ Cabal deepseq filepath ghcPaths xhtml haddockLibrary ];
  testDepends = with haskellPackages; [ Cabal deepseq filepath hspec QuickCheck ];
  isLibrary = true;
  isExecutable = true;
  enableSplitObjs = false;
  noHaddock = true;
  doCheck = true;
})
[shana@lenalee:~/programming/haddock]$ cat haddock-library/default.nix
{ haskellPackages ? (import <nixpkgs> {}).myHaskellPackages_ghc763
}:
let
  inherit (haskellPackages) cabal deepseq QuickCheck hspec baseCompat;
in
cabal.mkDerivation (self: {
  pname = "haddock-library";
  version = "1.1.0";
  src = /home/shana/programming/haddock/haddock-library;
  testDepends = [ QuickCheck hspec baseCompat ];
  buildDepends = [ deepseq ];
  isLibrary = true;
  isExecutable = false;
  enableSplitObjs = false;
  doCheck = true;
})

There are a couple of things going on here. Firstly, you can see that haddock-library by default uses GHC 7.6.3: haskellPackages ? (import <nixpkgs> {}).myHaskellPackages_ghc763. This is fine but when I’m working with Haddock itself, I want to make sure this gets built with same version as haddock, so I have

, haddockLibrary ? (import /home/shana/programming/haddock/haddock-library
    { haskellPackages = haskellPackages; })

This makes sure we use the same set of packages in both so when haddock uses GHC HEAD then so does haddock-library. To nix enthusiasts out there, I’m aware I can use ‘inhert’, just didn’t get around to it.

Now whenever I change things under haddock-library and drop into haddock shell, it will automagically get rebuilt.

Better yet, I do this with GHC itself! If you’ll notice, I’m importing (import <nixpkgs> {}).myHaskellPackages_ghcHEAD. If you look in my nixpkgs config you’ll find some incantations of following nature:

{ pkgs }:

{ packageOverrides = self: with pkgs; rec {

  haskellPackages_ghcHEAD = self.haskell.packages {
    ghcPath = /home/shana/programming/ghc;
    ghcBinary = self.haskellPackages.ghcPlain;
    prefFun = self.haskell.ghcHEADPrefs;
  };

  …

What’s going on here? Well, a few things. First I’m overwriting a thing called ghcPath to /home/shana/programming/ghc. This points to my local GHC HEAD checkout. In there I have another nix expression which describes how to build GHC HEAD. This means that yes, I am able to have Haddock depend on a checkout of GHC itself. Here is that GHC expression in full:

{ pkgs ? (import <nixpkgs> {})
, stdenv ? pkgs.stdenv
, ghc ? pkgs.ghc.ghc782
, perl ? pkgs.perl
, gmp ? pkgs.gmp
, ncurses ? pkgs.ncurses
, happy ? pkgs.haskellPackages.happy
, alex ? pkgs.haskellPackages.alex
, automake ? pkgs.automake
, autoconf ? pkgs.autoconf
, git ? pkgs.git
, libxslt ? pkgs.libxslt
, libxml2 ? pkgs.libxml2
, python ? pkgs.python
}:

stdenv.mkDerivation rec {
  name = "ghc-${version}";
  version = "7.9.20140624";

  src = "/home/shana/programming/ghc";

  buildInputs = [ ghc perl gmp ncurses automake autoconf
                  git happy alex libxslt libxml2 python ];

  enableParallelBuilding = true;

  buildMK = ''
    libraries/integer-gmp_CONFIGURE_OPTS += --configure-option=--with-gmp-libraries="${gmp}/lib"
    libraries/integer-gmp_CONFIGURE_OPTS += --configure-option=--with-gmp-includes="${gmp}/include"
    DYNAMIC_BY_DEFAULT = NO
    BuildFlavour = quick
  '';

  preConfigure = ''
    echo "${buildMK}" > mk/build.mk
    perl boot
    sed -i -e 's|-isysroot /Developer/SDKs/MacOSX10.5.sdk||' configure
  '' + stdenv.lib.optionalString (!stdenv.isDarwin) ''
    export NIX_LDFLAGS="$NIX_LDFLAGS -rpath $out/lib/ghc-${version}"
  '';

  configureFlags = "--with-gcc=${stdenv.gcc}/bin/gcc";

  # required, because otherwise all symbols from HSffi.o are stripped, and
  # that in turn causes GHCi to abort
  stripDebugFlags = [ "-S" "--keep-file-symbols" ];

  meta = {
    homepage = "http://haskell.org/ghc";
    description = "The Glasgow Haskell Compiler";
    maintainers = [
      stdenv.lib.maintainers.marcweber
      stdenv.lib.maintainers.andres
      stdenv.lib.maintainers.simons
    ];
    inherit (ghc.meta) license platforms;
  };

You don’t have to be able to understand this but know that whenever I want to update my GHC HEAD, all I have to do is to update the repository (through usual sync-all GHC script) and then bump up the version in above expression. Now if I go to drop into a nix-shell for Haddock, it will notice the change and build GHC HEAD.

Now to explain another bit of my config:

  myHaskellPackages_ghcHEAD = pkgs.recurseIntoAttrs (haskellPackages_ghcHEAD.override {
    extension = se : su : {
      syb = se.callPackage /home/shana/programming/nixpkgs/pkgs/development/libraries/haskell/syb/0.4.2.nix {};
      vty_5_1_0 = se.callPackage /home/shana/programming/nix-project-defaults/vty/5.1.0.nix {};
      mtl = se.callPackage /home/shana/programming/nix-project-defaults/mtl/2.2.1.nix {};
      testFrameworkSmallcheck =
        se.callPackage /home/shana/programming/nix-project-defaults/test-framework-smallcheck {};
    };
  });

  …
}; }

What I’m doing here is defining or overwriting packages in the Haskell package set: as you can see, I’m defining vty_5_1_0 and setting mtl default to 2.2.1. Why? They were either not at that moment in my version of nixpkgs (my channel hasn’t caught up) or I wanted to use different defaults. It’s as easy as the above. This brings me to the next point.

What happens when nixpkgs doesn’t have something you need?

  1. Create an expression for it. This is as easy as using cabal2nix. If it’s on hackage, it’s even easier:

    [shana@lenalee:~/programming/haddock]$ cabal2nix cabal://text
    { cabal, deepseq, HUnit, QuickCheck, random, testFramework
    , testFrameworkHunit, testFrameworkQuickcheck2
    }:
    
    cabal.mkDerivation (self: {
      pname = "text";
      version = "1.1.1.3";
      sha256 = "1yrzg449nbbzh2fb9mdmf2jjfhk2g87kr9m2ibssbsqx53p98z0c";
      buildDepends = [ deepseq ];
      testDepends = [
        deepseq HUnit QuickCheck random testFramework testFrameworkHunit
        testFrameworkQuickcheck2
      ];
      meta = {
        homepage = "https://github.com/bos/text";
        description = "An efficient packed Unicode text type";
        license = self.stdenv.lib.licenses.bsd3;
        platforms = self.ghc.meta.platforms;
      };
    })
  2. Point to it somehow from your project. Two main ways are to either add it to your package base (as seen in my config snippet) or do it directly from a project (as seen from my haddock expression snippet).

  3. Make a pull request to nixpkgs so everyone can benefit. Please read contribution NixOS wiki page on how to contribute.

So is this better than cabal sandbox? In my opinion, yes, here’s why I think so:

Even hakyll which is a Haskell program that will generate a page from this Markdown post is going to be used by nix-shell -p haskellPackages_ghc763.ghc -p haskellPackages_ghc763.hakyll --pure: I don’t need it day to day so I’ll just let it get garbage collected at next opportunity.

The downsides of using nix-shell for Haskell projects:

Summary

I’ll give a breakdown of what I like and dislike about nix and NixOS so far.

What I like:

What I dislike:

It may look like there are many dislikes but they are mostly annoyances or my incompetence. I definitely would recommend nix or even NixOS if you are already considering a switch. I have to say that I can not recommend switching to NixOS if you need your machine in development-ready mode next morning because it can take a few days to get everything going just the way you need it. I don’t have this worry with nix itself however which you can install alongside your distro. If you’re a working man, I believe you could set up NixOS in a VM first and then simply carry over the config when you have everything ready.

In general I find that I never worry whether my package database will screw up or anything like that.

If you’re interested, please swing by #nixos on Freenode. This and the mailing list is the majority of my help has been coming from.

It’s a bit of a hectic post so please feel free to contact me if you have questions and I’ll try to answer to best of my knowledge. Note that I’ll almost certainly not see and/or not reply to questions on reddit if this is to find its way there, sorry.