Haskell Ecosystem Workshop
Sam Derbyshire, Well-Typed
June 7th, 2024
Setup.hs
interface and its shortcomings.Cabal
library interface, and how to leverage it
(cabal-install
, HLS).⭲
The Cabal specification (2005) was designed to allow Haskell tool authors to package their code and share it with other developers.
The Haskell Package System (Cabal) has the following main goal:
- to specify a standard way in which a Haskell tool can be packaged, so that it is easy for consumers to use it, or re-package it, regardless of the Haskell implementation or installation platform.
⭲
base >= 4.17 && < 4.21, lens ^>= 5.3
)
build-type: Simple
)
⭲
Cabal
libraryThe Cabal
library (including Cabal-syntax
)
provides:
.cabal
file format,hc-pkg
installed package info format,Setup.hs
CLI,⭲
To implement the Cabal
spec, a Haskell compiler
(hc
) must provide a package registration program
(hc-pkg
).
The details of package registration are laid out in the Cabal specification.
> ghc-pkg describe attoparsec --package-db=<cabal-store>/<ghc-ver>/package.db
name: attoparsec
version: 0.14.4
visibility: public
id: attoparsec-0.14.4-a723f6157cb40470c8790185a66d38e55c777104
abi: 52f592e79352f6fa9575bbf0d73225b8
exposed: True
exposed-modules: [...]
hidden-modules: [...]
depends: array-0.5.6.0-4cb3 [...]
[...]
⭲
Note that, rather confusingly, one does not register packages with this tool, only individual units.
Setup
interfaceTo implement the Cabal specification, the build system of a package
needs only provide the Setup
command-line interface,
consisting of a Setup
executable which supports
./Setup <cmd>
invocations.
<cmd> |
description |
---|---|
configure |
resolve compiler, tools and dependencies |
build /haddock /repl |
prepare sources and build/generate docs/open a GHCi session |
test /bench |
run testsuites or benchmarks |
install /register |
move files into final location/register libraries in the
PackageDB |
sdist |
create an archive for distribution/packaging |
clean |
clean local files (local package store, local build artifacts, …) |
⭲
Each command comes with its own set of flags, e.g. Cabal
ConfigFlags
(by far the most complex).
In practice, ./Setup configure
takes many flags, with
the configuration being preserved for subsequent invocations (which
barely take any flags, e.g.
./Setup build -v2 --builddir=<dir>
).
⭲
./Setup
In a build plan, we must manually build all dependencies in dependency order.
To build individual units:
./Setup configure <compName> <confArgs>
./Setup build --builddir=<buildDir>
./Setup haddock --builddir=<buildDir> <haddockArgs>
./Setup copy --builddir=<buildDir> --destDir=<destDir>
./Setup register --builddir=<buildDir> --gen-pkg-config=<unitPkgReg>
(libs only)
hc-pkg register --package-db=<pkgDb> <unitPkgRegFile>
(libs only)
⭲
Setup
: the tricky parts./Setup configure
--package-db=<pkgDb>
--cid=<unitId>
--dependency=<depPkgNm>:<depCompNm>=<depUnitId>
./Setup
build-tool-depends
executables in
PATH
<buildTool>_datadir
environment variables.⭲
Setup
CLI.cabal-install
and the
Setup
executable use a different version of the
Cabal
library (Distribution.Client.Setup.filterConfigureFlags
).⭲
Setup.hs
too generalEach package brings its own (possibly completely custom) build
system.
This limits what cabal-install
or HLS can do in
multi-package projects.
In practice, all packages use the Cabal
library:
build-type: Simple
./Setup configure
= Cabal
library configure
./Setup build
= Cabal
library build
build-type: Custom
using UserHooks
.
⭲
build-type: Custom
example
(singletons-base
)See the
Setup.hs
file for singletons-base
.
⭲
Instead we would be better off with a Haskell library
interface for customising the build system of a package, and
make use of this in cabal-install
instead of going through
the Setup
CLI.
⭲
The Hooks
build-type provides a new way to customise how
a package is built: use the Cabal
library, but with custom
hooks that augment (but don’t override) what the Cabal
library does.
⭲
cabal-version: 3.14
...
build-type: Hooks
...
custom-setup
setup-depends:
base >= 4.18 && < 5,
Cabal-hooks >= 0.1 && < 0.2
module SetupHooks where
-- Cabal-hooks
import Distribution.Simple.SetupHooks
setupHooks :: SetupHooks
setupHooks =
noSetupHooks
{ configureHooks = myConfigureHooks
, buildHooks = myBuildHooks }
⭲
There are three hooks into the configure phase:
type PreConfPackageHook = PreConfPackageInputs -> IO PreConfPackageOutputs
./configure
-style logic
type PostConfPackageHook = PostConfPackageInputs -> IO ()
type PreConfComponentHook = PreConfComponentInputs -> IO PreConfComponentOutputs
⭲
See the configure hooks of the custom-preproc
example.
⭲
See the configure hooks of the system-info
example.
⭲
The general mechanism for preparing source files for compilation is that of pre-build rules (Hackage).
Thinking in terms of custom pre-processors:
⭲
singletons-base
, using Hooks
See the pre-build rules for singletons-base
migrated to build-type: Hooks
.
⭲
See the pre-build rules of the custom-preproc
example (myPreBuildRules
).
⭲
Plan:
cabal-install
.⭲
To integrate packages with build-type: Hooks
through a
library interface, we compile the SetupHooks
module into a
separate executable with which we communicate via a CLI.
Note: Uses new CommunicationHandle
API
from process
.
The API for build-type
Hooks is not a
CLI, it is a library interface.
It is just cabal-install
which internally compiles
SetupHooks.hs
to a separate executable and uses a CLI.
⭲
How do we compile a package pkg
with
build-type: Hooks
when:
pkg
declares
setup-depends: Cabal == 3.14.*
,cabal-install
is linked against Cabal
3.16.1.0
?⭲
Structured
We use Cabal
’s
Structured
mechanism to ensure that both sides of the
IPC channel agree on the Binary
instances used.
⭲
To provide compatibility between different Cabal
library
versions, we propose to use private dependencies.
cabal-install
would be linked against multiple versions
of the Cabal
library, and would come bundled with internal
adapter functions.
projectOut_localBuildInfo_V3_16_V3_14
:: V3_16.LocalBuildInfo -> V3_14.LocalBuildInfo
projectOut_PreConfComponentInputs_V3_16_V3_14
:: V3_16.PreConfComponentInputs -> V3_14.PreConfComponentInputs
inject_preConfComponentOutputs_V3_14_V3_16
:: V3_14.PreConfComponentOutputs -> V3_16.PreConfComponentOutputs
⭲
The external hooks executable supports three queries for pre-build rules:
preBuildRules
hookMap RuleId RuleBinary
Details about how e.g. HLS would leverage this are given in the
Cabal-hooks
documentation.
⭲
When cabal-install
invokes ./Setup
, it sets a bunch of process-global
state.
7b90583
)ee11ac6
)
logging handle (Cabal
#9987)
⭲
Custom
and Hooks
build types) are treated as a whole.This is a long-standing flaw in the implementation of
cabal-install
.
The task is up for grabs at Cabal
#9986.
⭲
Slides available online: sheaf.github.io/cabal-talk.
Cabal tickets (in increasing order of difficulty):
cabal-install
: Cabal
#10064.Cabal
: Cabal
#9987.Cabal
#9986.Also:
Setup Hooks reference material: