Reproducible builds of the GHC we use: an upstream GHC release plus the patches in
patches/, packaged as a ghcup-installable bindist.
Two variants are built from the same source tree:
| variant | GHC Dynamic |
what it's for |
|---|---|---|
dynamic |
YES |
matches a stock upstream bindist |
static |
NO |
Cabal stops emitting -dynamic-too, worth ~⅓ of compile time on a large codebase |
0001-rts-track-per-thread-CPU-time.patch— addsGHC.Stats.getMyThreadCPUTime(from commit07d60149on a GHC 9.2.7 branch). Every variant carries it.
Patches are exported from branches predating GHC 9.4's includes/ → rts/include/ move;
build.sh rewrites those paths when the tree has rts/include.
cabal install alex happy # plus: build-essential autoconf automake python3 xz-utils libgmp-dev libnuma-dev
./build.sh --flavour static -j16 # ~15 min on 32 coresOutput lands in dist/ with a .sha256 beside it. Install it with:
ghcup install ghc -u <url-of-tarball> 9.6.4 --setCI builds both variants in an ubuntu:22.04 container — that has to match the oldest glibc
the compiler will run on, or it won't start there. Pushing a ghc-* tag attaches both
tarballs to a GitHub release; workflow_dispatch builds them as artifacts without releasing.
Add the source checksum from https://downloads.haskell.org/~ghc/<version>/SHA256SUMS to the
case in build.sh (or pass --ghc-src-sha256), then re-check that each patch still applies.
Removing -dynamic-too measurably helps. Measured on one large library (~2900 modules,
heavy Template Haskell), -O0, dynamic → static:
| workers | wall | CPU |
|---|---|---|
-j4 |
225.2s → 146.1s | 780s → 492s |
-j8 |
155.7s → 105.6s | 940s → 610s |
-j16 |
135s → 94.5s | 1390s → 927s |
with its test suite still green. Note static -j8 beats dynamic -j16 on wall clock using
less than half the CPU.
The catch: with no dynamic libraries, Template Haskell loads dependency object code through GHC's own RTS linker instead of the system dynamic linker, and that linker is weaker. Expect to need per-package workarounds:
flags: {bitvec: {simd: false}}— otherwiseunknown symbol '__cpu_model', a libgcc CPU-feature builtin.ghc-options: {zstd: -optc-DZSTD_TRACE=0}— otherwise an undefined weak symbol (ZSTD_trace_compress_begin) that the RTS linker won't resolve to 0.+RTS -xm20000000 -RTSin the GHC options of anything downstream of a very large library — otherwiseloadObj "HS<pkg>...o": failed, because the linker can't find low-address room to mmap the merged library object (~388 MB in our case). Building the library itself is fine: that loads individual module objects, not the merged one.- Any dependency whose C code leans on weak or libgcc symbols fails the same way, at Template Haskell time, until someone adds a flag for it.
-xm is a hint, not a fix: it works because that address happened to have room. It is
sensitive to the library object growing, to how many dependency objects were loaded first,
and to the RTS heap flags on the same command line. The durable fix is splitting a library
that large so no single merged object is.
Untested with the static variant: stack ghci, executable components, and HLS. Only
stack build and stack test were validated.