Migrating from Make to Hadrian (for packagers)
Sam Derbyshire - 2022-08-05
As the Hadrian build system for GHC has reached maturity and the old Make-based build system is becoming increasingly costly to maintain, the GHC maintenance team has decided that it is finally time to remove GHC’s Make-based build system. GHC 9.4 will be the last release series compatible with Make, which will be limited to booting with GHC 9.0. From 9.6 onwards, the only supported way to build GHC will be to use Hadrian.
This blog post will give an overview of using Hadrian, which should help packagers migrate from the old Make-based build system.
The Hadrian build system
Hadrian is a modular, statically-typed, extensible build system for GHC, introduced in the paper Non-recursive Make Considered Harmful. It consists of a Haskell executable implemented using the shake library, and is used to configure and build GHC.
Building Hadrian
Contributors to GHC will be accustomed to running the ./hadrian/build
script,
which builds and runs Hadrian. This script calls out to cabal
, which fetches
the dependencies of the Hadrian package from Hackage before building the
resulting Haskell executable. While this is convenient for developers, it isn’t
appropriate for build environments in which one doesn’t have access to the
network (e.g. in order to enforce a hermetic build environment). For that
reason, Hadrian provides a set of scripts for bootstrapping the build system
from source tarballs. These can be found in the hadrian/bootstrap
directory.
Bootstrapping Hadrian
The Hadrian bootstrap scripts are driven by a set of precomputed build plans; these depend on the version of the bootstrap GHC being used. A typical workflow might look like the following:
- Locally:
- Choose a build plan appropriate for the bootstrap GHC version, such as
hadrian/bootstrap/plan-bootstrap-8.10.7.json
. (These build plans can also be generated manually from acabal-install
build plan; seegenerate_bootstrap_plans
) - Fetch the sources needed by the build plan:
bootstrap.py fetch -w <path_to_ghc> --deps plan-bootstrap-8.10.7.json -o 8_10_7_bootstrap_sources.tar.gz
- Choose a build plan appropriate for the bootstrap GHC version, such as
- In the build environment:
- Provision the
bootstrap-sources
tarball generated above.
- Provision the
- In your GHC build script:
- Build Hadrian using the bootstrap script:
bootstrap.py -w <path_to_ghc> --bootstrap-sources 8_10_7_bootstrap_sources.tar.gz
- Build GHC using the resulting Hadrian executable, located by default in
bin/hadrian
, e.g.bin/hadrian -j --flavour=perf+debug_info -w <path_to_ghc>
- Build Hadrian using the bootstrap script:
An example of how to use these bootstrap scripts can be seen in the ghcs-nix
repository.
This repository contains Nix expressions specifying how to build many GHC
versions, with both Make and Hadrian.
From now on, we will assume that you have built Hadrian (either via
./hadrian/build
or via bootstrapping), referring to the hadrian
executable
agnostically.
Using Hadrian
How does Hadrian replace make?
To build GHC, we begin as before by running ./boot
(if necessary, i.e. a
configure
file doesn’t already exist) and then ./configure <args>
. As with
Make, the build environment is determined by the configure script, which will
read provided arguments as well as environment variables. For example, the
selection of the bootstrap compiler is done via the GHC
environment variable,
and the selection of the C compiler uses the CC
environment variable. This is
unchanged, and details can be found on the GHC
wiki.
Once the configure script is run, we replace make
commands with hadrian
commands. The fundamental command to build GHC with Hadrian is
hadrian <args>
Hadrian stages
GHC is a self-hosted compiler. This means that we need to provide a GHC executable in order to compile GHC. In Hadrian, this is done in stages:
- The
stage0
compiler is the bootstrap compiler: a user-provided executable which will be used to compile GHC. The bootstrap compiler is chosen via theGHC
variable passed to theconfigure
script. - The
stage1
compiler is the compiler built using thestage0
compiler; it runs on the build platform and produces code for the target platform. The stage1 compiler is limited in that it does not support dynamic code loading via the internal bytecode interpreter. - The
stage2
compiler is the compiler built using thestage1
compiler; it runs on the target platform. Thestage2
compiler is necessary for the implementation of Template Haskell and GHC plugins.
In Hadrian, build artifacts are put in a subdirectory of the build folder (by
default, _build
) corresponding to the stage of the compiler used to perform
the build. This means that the stage2
compiler will be found (if using the
default build directory, _build
) at _build/stage1/bin/ghc
.
Hadrian provides meta-targets which can be used to build particular subsets of the compiler. A typical Hadrian command, which builds a library or executable for a given stage, looks like
hadrian <stage>:{lib,exe}:<package name>
For example, hadrian stage2:lib:base
will build the stage2
base
library,
and put it into the _build/stage1
subdirectory.
Flavours and flavour transformers
A Hadrian build flavour is a pre-defined collection of build settings that
fully define a GHC build. These are described
here.
The flavour being used determines the ways in which GHC and its libraries will
be built, as described in the Hadrian documentation.
This replaces the variables of the make
build system such as GhcLibWays
,
DYNAMIC_GHC_PROGRAMS
, DYNAMIC_BY_DEFAULT
.
A flavour is set using the --flavour
command-line argument, e.g.
hadrian/build --flavour=perf
. As a packager you probably want to use either
the release
or perf
flavour:
flavour name | description |
---|---|
perf |
A fully optimised bindist |
release |
The same configuration as perf , but with additional build products such as interface files containing Haddock docs |
Flavours can be modified using flavour transformers. For example, the
profiled_ghc
flavour transformer compiles the GHC library and executable with
cost-centre profiling enabled. One can, e.g., apply the profiled_ghc
transformer to the perf
flavour with hadrian --flavour=perf+profiled_ghc
.
Make variable | Hadrian flavour transformer |
---|---|
GhcProfiled |
profiled_ghc |
GhcDebugged |
debug_ghc |
SplitObjs |
split_sections |
The full list of supported flavour transformers is available here.
Building GHC for distribution
Packagers will be interested in the binary-dist
and binary-dist-dir
Hadrian
targets. For example, the command
hadrian/build --flavour=release binary-dist
will produce a complete release binary distribution tarball, while the
binary-dist-dir
target produces the directory only (not the tarball). The
resulting bindist will be placed in _build/bindist
.
When building the binary-dist
target, documentation (namely, Haddock
documentation and GHC’s Sphinx-built user’s
guide) will be built by default. Building of documentation can be disabled
using Hadrian’s --docs=...
command-line flag. If you don’t want to build
documentation, there are options to disable building various parts of the
documentation.
For example, if you don’t have Sphinx available, you can disable the parts of the documentation which require it:
# Build only the documentation for the base package, without using sphinx
hadrian {..} docs:base --docs=no-sphinx
Further information about configuring the documentation built by Hadrian can be found in the Hadrian readme.
Large-integer implementation
GHC supports several implementations of the Integer
/Natural
types and
operations on them. The selection of the implementation is done using the
--bignum
Hadrian argument, e.g. --bignum=gmp
to use the GMP
library, or --bignum=native
to use a pure-Haskell
implementation.
Key-value settings
While we expect that the mechanisms described above will suffice for most builds, Hadrian also provides a fine-grained key-value configuration mechanism for modifying the command-lines passed to each of the tools run by Hadrian. For instance, one can pass an additional argument to all GHC invocations via:
hadrian {..} "*.*.ghc.*.opts += -my-ghc-option"
Passing an additional option when compiling the ghc
library only:
hadrian {..} "*.ghc.ghc.*.opts += -my-ghc-option"
These settings can also be placed in a hadrian.settings
file in the build
root (by default _build
), instead of passing them in the command line.
Hadrian currently supports the following key-value settings
(<stage> or *).(<package name> or *).ghc.{hs, c, cpp, link, deps, *}.opts
Arguments passed to GHC invocations.hs
for arguments passed to GHC when compiling Haskell modulesc
for arguments passed to the C compilercpp
for arguments passed to the C++ compilerlink
for arguments passed during linkingdeps
for arguments to aghc -M
command, which outputs dependency information between Haskell modules
(<stage> or *).(<package name> or *).cc.{c, deps, *}.opts
Arguments passed directly to the C compiler.(<stage> or *).(<package name> or *).cabal.configure.opts
Arguments passed to the cabal configure step.(<stage> or *).(<package name> or *).hsc2hs.run.opts
Arguments passed when running Hsc2Hs.
These Hadrian key-value settings are useful to replace the assignment of Make variables, even though Hadrian is not intended to be a one-to-one replacement of Make; recovering the behaviour with Hadrian might require a few tweaks.
Consider for example the way that Make passes flags to GHC when compiling
source Haskell files. Make has several different variables, such as
SRC_HC_OPTS
, WAY_<way>_<pkg>_OPTS
, EXTRA_HC_OPTS
. These are passed in
order, with later flags overriding previous ones. With Hadrian, things are much
simpler, and one can usually achieve the same goal by simply setting the
*.*.ghc.hs.opts
Hadrian key-value setting.
The following table serves as a general guideline in migrating the use of Make variables (bearing the above caveats in mind):
Make variable | Hadrian key-value setting |
---|---|
GhcLibOpts |
*.*.ghc.*.opts |
GhcRtsHcOpts |
*.*.rts.*.opts |
SRC_HC_OPTS , EXTRA_HC_OPTS |
*.*.ghc.hs.opts |
SRC_CC_OPTS , EXTRA_CC_OPTS |
*.*.ghc.c.opts with -optc prefix |
SRC_CPP_OPTS , EXTRA_CPP_OPTS |
a combination of *.*.ghc.c.opts with -optc prefix and *.*.cc.c.opts |
SRC_LD_OPTS , EXTRA_LD_OPTS |
*.*.ghc.link.opts with -optl prefix |
<pkg>_EXTRA_LD_OPTS |
*.<pkg>.ghc.link.opts with -optl prefix |
<pkg>_CONFIGURE_OPTS |
*.<pkg>.cabal.configure.opts |
utils/hsc2hs_dist-install_EXTRA_LD_OPTS |
*.*.hsc2hs.run.opt with -L prefix |
To pass module-specific or way-specific options, e.g. passing a C pre-processor
option when compiling specific modules in a certain way
(as when using a
Way_<way>_<pkg>_OPTS
Make variable), please use the programmatic interface
described below.
Programmatic configuration
If the above configuration mechanisms aren’t sufficient, it is also possible to directly add new configurations to Hadrian. This allows finer-grained changes, such as changing the options when compiling a specific module or set of modules. If you really need to do this, you can read about it in the Hadrian manual. A good starting place to look for inspiration is Settings.Packages, which contains the arguments used to build GHC and the libraries it depends upon. The documentation for Shake is also a helpful resource, as Hadrian uses the Shake EDSL to implement its build rules.
Further support
If you are having any issues with packaging GHC after these changes, or find yourself needing to use the programmatic interface, please open an issue on the issue tracker, so that we can work together to modify Hadrian for your needs.