Static linking on Nix with GHC 9.6
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.