build-env
build-env
is a utility to build a set of Cabal packages (computed
by cabal-install
's solver) into a free-standing package database.
This enables the compilation of Haskell packages (with their dependencies)
in hermetic build environments.
Contents
Example
$ build-env build lens -f sources -o install -j8
$ ghci -package-db install/package.conf -package lens
ghci> :ty Control.Lens.Lens
Control.Lens.Lens
:: Control.Lens.Type.Lens s t a b
-> Control.Lens.Reified.ReifiedLens s t a b
In this example, the build-env build
invocation:
- computes a build plan for
lens
,
- fetches
lens
and its dependencies into the sources
directory,
- configures and builds all the packages, with up to
8
units building
concurrently, and registers the libraries into the package database at
install/package.conf
.
We then tell ghci
to use this package database, making lens
available.
Commands
build-env
has three distinct modes of operation:
plan
computes a build plan, outputting a plan.json
Cabal plan.
fetch
fetches sources.
build
executes a build plan.
Each command subsumes the functionality of the previous ones in the above list.
In particular, in the previous example, the build
command computed a build
plan, fetched the sources, and built the plan. The same could have been achieved
with three separate invocations:
$ build-env plan lens -p lens-plan.json
$ build-env fetch -p lens-plan.json -f sources
$ build-env build -p lens-plan.json -f sources -o install -j8 --prefetched
Being able to separate these steps affords us some extra flexibility, as
subsequent sections will explain.
Note: it is preferable to pass a previously computed build plan when calling
build-env build --prefetched
: an invocation of the form build-env build a b c --prefetched
will compute a new build plan, and this build plan could be different from
the plan that was previously fetched, for example if you have run cabal update
in the meantime.
Another option is to pass --index-state
, which pins down a specific Hackage
index state.
Parallelism
build-env
supports parallel builds in much the same way as cabal-install
does:
-jN
controls how many packages are built concurrently.
--configure-arg --ghc-option=-jN
controls the parallelism afforded to
GHC invocations when building individual packages.
--jsem N
allows build-env
and GHC to coordinate usage of CPU resources.
This is usually the fastest, but it requires:
- GHC >= 9.8,
- that GHC and
build-env
have been compiled against the same libc implementation
(see GHC issue #25087).
What does build-env
do?
- plan:
build-env
calls cabal build --dry-run
on a dummy
project with the dependencies and constraints that were asked for.
This produces a Cabal plan in the form of a plan.json
, which build-env
parses.
Required executables: cabal
, ghc
.
- fetch:
build-env
calls cabal get
on each package in the build plan.
Required executables: cabal
.
- build:
build-env
builds the dependencies by compiling their Setup
scripts and calling Setup configure
, Setup build
,
Setup haddock
(optional), Setup copy
, and, for libraries, Setup register
and ghc-pkg register
.
Required executables: ghc
, ghc-pkg
.
Note that, when building packages, build-env
passes the following information
when running the Setup
script:
with-compiler
, prefix
and destdir
options supplied by the user,
cid
, datadir
, datasubdir
, bindir
and builddir
options supplied by build-env
,
- package flags and
dependency
arguments, obtained from the build plan,
<pkg>_datadir
environment variables supplied by build-env
.
These options cannot be overridden. If you absolutely need to change some of
these, please file a bug on the issue tracker asking for this customisability.
Hermetic builds
When working in a hermetic build environment, we might need to compute a build
plan and fetch all the dependencies first, before doing an offline build.
This can be achieved by two separate build-env
invocations.
In a local environment (with access to internet):
$ build-env fetch lens -f sources --output-plan plan.json
In a build environment in which build-env
has been provisioned:
$ build-env build --prefetched -p plan.json -f sources -o install -j8
In this example:
- the
fetch
command computes a plan which is written to
plan.json
(using the Cabal JSON plan format), and fetches all
the required sources, putting them into the sources
directory;
- the
build
command reads in the build plan and performs the build.
Narrowing a build down (for debugging)
When attempting to execute a large build plan, it can be useful to be able
to refine the build down to a small set of problematic packages, for debugging
purposes. For this, you can use --only
, e.g.:
$ build-env build --prefetched -p plan.json -f sources -o install --only badPackage
Instead of building the full plan from plan.json
, this will instead restrict
to only building units from package badPackage
and their transitive dependencies.
Multiple --only
arguments combine additively, e.g. --only pkg1 --only pkg2
will build the units from the build plan that belong to pkg1
or pkg2
, and
transitive dependencies thereof.
Note that --only
only affects which packages are built; it does not change
the computation of the build plan nor which packages are fetched.
You can also resume a build where it left off by passing --resume
; this will
avoid rebuilding any of the units that have already been registered in the
package database.
Bootstrapping
In the example from ยง Hermetic builds, we ran
build-env build
to perform a build. However, this requires that the build
environment provision build-env
.
To avoid this requirement, it is also possible to ask build-env
to output
a shell script containing the build steps to execute.
In a local environment (with access to internet):
$ build-env build lens -f sources -o install --script build_lens.sh
In a build environment:
$ ./build_lens.sh
The downside is that we lose any parallelism from this approach, as the
generated build script is inherently sequential.
One particularly useful application is the bootstrapping of build-env
itself,
as this supplies a mechanism for the provisioning of build-env
in build
environments.
Bootstrap arguments
If your build environment provides additional variables that need to be passed
to the build script, you can pass them to the shell script using variables.
For example, in the local environment, you can first obtain all the information
needed to return a build script:
$ build-env fetch lens -f sources --output-plan myPlan.json
Next, compute a build script containing references to variables:
$ build-env build -p myPlan.json -f sources -o install --prefetched --configure-arg \$myArgs --script script.sh
This will output a script which passes $myArgs
to each Setup configure
invocation. In the build environment, you can then set the value of $myArgs
before running the shell script script.sh
. Note that the build script
does not insert additional quotation marks around $myArgs
, which allows
passing multiple arguments at once (this is crucial when one doesn't know
the number of arguments ahead of time).
If you want to set --fetchdir
, --prefix
or --destdir
to variables,
you should pass the --variables
flag. This will set these to the values
$SOURCES
, $PREFIX
and $DESTDIR
in the output shell script, respectively.
These should be set to absolute paths before running the script.
The script will also use $GHC
and $GHCPKG
variables for the compiler.
Specifying packages
Instead of passing the required packages through the command line,
it can be more convenient to ask build-env
to read them from files:
$ build-env build --seeds SEEDS --freeze cabal.config -f sources -o install
This will build a plan for the packages specified in the SEEDS
file,
with versions of packages that aren't in the SEEDS
file constrained as
specified by the cabal.config
file.
Each line in the seeds file should be one of the following:
-
A Cabal unit, in the format unit +flag1 -flag2 >= 0.1 && < 0.3
.
A unit can be of the form pkgName
, lib:pkgName
, exe:pkgName
,
pkgName:lib:compName
, ... as per cabal
component syntax.
The unit name must be followed by a space.
Flags and constraints are optional.
When both are present, flags must precede constraints.
Constraints must use valid Cabal constraint syntax.
-
An allow-newer specification such as:
allow-newer: pkg1:pkg2,pkg3:base,*:ghc
This uses Cabal allow-newer
syntax.
-
An empty line or comment (starting with --
).
The cabal.config
file should use valid cabal.project
syntax, e.g.:
constraints: pkg1 == 0.1,
pkg2 >= 3.0
Only the constraints are processed; everything else from the cabal.config
file is ignored.
Local packages
To specify that a package should be found locally rather than fetched from
Hackage, use --local
:
build-env build myPkg --local myPkg=../pkgs/myPkg
The part to the right of the =
sign corresponds to what you would write
in a cabal.project
file: the directory in which myPkg.cabal
is located.
If this is a relative path, it will be interpreted relative to the working
directory of the build-env
invocation, which can be overriden by passing
--cwd <dir>
(works like the -C
argument to make
).