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.