Static linking on Nix with GHC 9.6

Posted on May 22, 2024

I like to use Nix to do static builds of Haskell programs for distribution. I find it very needs suiting. Haskell is the best general-purpose programming language, and Nix is the best build tool, and I like things that are good.

The largest project on which I work regularly is Futhark, which has just this setup, as previously described here. To ensure reproducibility, the version of Nixpkgs is pinned (using niv, but that detail doesn’t matter). Every once in a while I manually update the version of Nixpkgs we use (also with niv, and again that doesn’t matter, but niv is really good so I should shill it a bit). Sometimes that has no real effect, and sometimes it bumps the version of GHC. Sometimes that causes trouble. In particular, today when I updated Nixpkgs, it bumped the version of GHC 9.4 to 9.6, which resulted in a lot of linker errors. The reason seems to be that the GHC 9.6 in Nixpkgs expects libdw:

ghc --info|grep libdw
 ,("RTS expects libdw","YES")

This was not the case for GHC 9.4. Now, GHC is smart enough to link statically against libdw, but not to also link against all the dependencies of libdw - and there are many, and Nixpkgs does not make them available as static libraries by default. After a lot of trial and error, I came up with the following default.nix that will let you statically link a Haskell program with GHC 9.6 (and presumably later):

let pkgs =
      import (builtins.fetchTarball
        { url =
            "https://github.com/NixOS/nixpkgs/archive/a39290dfdf0a769a3fda56b61fdf40f7d9db7ea1.tar.gz"; }) {};
in pkgs.haskell.lib.overrideCabal
  (pkgs.haskell.packages.ghc910.callCabal2nix "bug" ./. {})
  ( _drv: {
    isLibrary = false;
    isExecutable = true;
    enableSharedExecutables = false;
    enableSharedLibraries = false;
    configureFlags = [
        "--ghc-option=-split-sections"
        "--ghc-option=-optl=-static"
        "--ghc-option=-optl=-lbz2"
        "--ghc-option=-optl=-lz"
        "--ghc-option=-optl=-lelf"
        "--ghc-option=-optl=-llzma"
        "--ghc-option=-optl=-lzstd"
        "--extra-lib-dirs=${pkgs.glibc.static}/lib"
        "--extra-lib-dirs=${pkgs.gmp6.override { withStatic = true; }}/lib"
        "--extra-lib-dirs=${pkgs.zlib.static}/lib"
        "--extra-lib-dirs=${(pkgs.xz.override { enableStatic = true; }).out}/lib"
        "--extra-lib-dirs=${(pkgs.zstd.override { enableStatic = true; }).out}/lib"
        "--extra-lib-dirs=${(pkgs.bzip2.override { enableStatic = true; }).out}/lib"
        "--extra-lib-dirs=${(pkgs.elfutils.overrideAttrs (old: { dontDisableStatic = true; })).out}/lib"
        "--extra-lib-dirs=${pkgs.libffi.overrideAttrs (old: { dontDisableStatic = true; })}/lib"
    ];
  }
  )

(This assumes you have a bug.cabal file actually defining some Haskell project.)

The important part is the various configureFlags I pass to GHC, in particular the extra linker flags (the -optl stuff) and the --extra-lib-dirs. The four different compression libraries and elfutils are for the benefit of libdw. Note also that I have to override a bunch of Nixpkgs derivations to get them to actually build the static libraries (and that I had to use four different techniques to do so). It’s a bit chaotic, but because of Nix, I’m actually not worried about pushing this into production, as it’ll keep working at least until the next time I bump Nixpkgs.

See also here for an example of this in practice.