7. How to use the Hooks build type
The Hooks build type allows customising the configuration and building
of a package using a collection of hooks into the build system.
It was introduced in Cabal 3.14 as a replacement for the
Custom build type.
See also:
7.1. Why use build-type: Hooks?
The main problem with build-type: Custom is that it is a wholesale replacement
of the entire build system, which means it doesn’t integrate with other tooling,
such as the Haskell Language Server.
build-type: Hooks aims to remedy this by guaranteeing that the main phases
for configuring/building a package are still the Cabal configure and
build phases, while allowing custom hooks to run in between.
Another advantage of build-type: Hooks is that it is defined as a Haskell
library interface, instead of the command-line interface of Setup.hs.
7.2. How to use build-type: Hooks
To define a package with build-type: Hooks, you will need the following:
Update your .cabal file to set build-type: Hooks, using a custom-setup
stanza to declare the dependencies of your SetupHooks:
cabal-version: 3.14
build-type: Hooks
custom-setup
setup-depends:
base >= 4.18 && < 5,
Cabal-hooks >= 3.16 && < 3.18
Then, define a Haskell module called SetupHooks.hs next to your .cabal
file. For example:
module SetupHooks where
import Distribution.Simple.SetupHooks ( SetupHooks, noSetupHooks )
setupHooks :: SetupHooks
setupHooks =
noSetupHooks
{ configureHooks = myConfigureHooks
, buildHooks = myBuildHooks }
This SetupHooks.hs module is where you define all of the hooks into the
build system. The following hooks exist:
Hooks into the configure phase:
Package-wide pre-configure hook. Used for custom
./configure-style logic.Package-wide post-configure hook. Used mostly to write information to disk for the per-component configure hooks.
Per-component pre-configure hook. Used to modify individual components in the package description, e.g. specifying per-component build flags or declaring autogenerated modules.
Hooks into the build phase (per component):
Pre-build rules. Used to define code generators or custom preprocessors, i.e. rules to generate source files.
Post-build hook. Used before linking to inject data into build artifacts.
Hooks into the install phase (per component). These are used to copy over extra files when installing an executable.
7.3. Basic hooks
With the exception of pre-build rules, all hooks take the form:
HookInput -> IO HookOutput
Specifying a hook thus amounts to providing a Haskell function of that type.
For the package-wide pre-configure, we have:
type PreConfPackageHook = PreConfPackageInputs -> IO PreConfPackageOutputs
and a typical hook would look like:
myPreConfPackageHook :: PreConfPackageHook
myPreConfPackageHook inputs@(PreConfPackageInputs {..}) = do
... -- custom logic goes here (e.g. querying system properties)
let myNewBuildOptions = ...
newConfiguredProgs = ...
return $
(noPreConfPackageOutputs inputs)
{ buildOptions = myNewBuildOptions
, extraConfiguredProgs = newConfiguredProgs
}
For per-component pre-configure hooks, we have:
type PreConfComponentHook = PreConfComponentInputs -> IO PreConfComponentOutputs
and a typical hook would look like:
myPreConfComponentHook :: PreConfComponentHook
myPreConfComponentHook inputs@(PreConfComponentInputs { component = comp, .. }) =
case componentName comp of
CLibName LMainLibName -> do
... -- custom logic goes here (e.g. parsing an XML schema)
let newModules = ...
myLdOptions = ...
return $
(noPreConfComponentOutputs inputs)
{ componentDiff =
ComponentDiff $ CLib $
emptyLibrary
{ exposedModules = newModules
, libBuildInfo =
emptyBuildInfo
{ autogenModules = newModules
, ldOptions = myLdOptions
}
}
}
_ -> return (noPreConfComponentOutputs inputs)
Post-build hooks and install hooks are similar.
Once you have defined all the hooks you need, finish by populating the
SetupHooks record:
module SetupHooks ( setupHooks ) where
import Distribution.Simple.SetupHooks
setupHooks :: SetupHooks
setupHooks =
noSetupHooks
{ configureHooks = noConfigureHooks
{ preConfPackageHook = Just myPreConfPackageHook
, preConfComponentHook = Just myPreConfComponentHook
}
}
7.4. Pre-build rules
A pre-build rule is a specification of what command to run in order to generate a collection of outputs (most commonly Haskell source modules). Each rule is registered separately, and rules can depend on each other. This allows for fine-grained recompilation logic, where only outdated rules are re-run.
A good starting point is the Hackage documentation for pre-build rules.
Let’s work through example usage of the API. First off, we define some code generators. These must be Haskell functions that take a single argument and return IO ().
{-# LANGUAGE DeriveAnyClass, DeriveGeneric, DerivingStrategies #-}
-- An example generator (a Haskell function).
runMyPP :: MyPPInput -> IO ()
runMyPP (MyPPInput {..}) = ...
-- Custom datatype for the argument to our single code generator.
data MyPPInput
= MyPPInput
{ ppVerbFlags :: VerbosityFlags
, ppSrcDir :: SymbolicPath Pkg (Dir Source)
, ppOutDir :: SymbolicPath Pkg (Dir Source)
, ppBaseName :: String
}
deriving stock ( Show, Eq, Generic )
deriving anyclass Binary
Here we’ve defined a single generator, runMyPP. The idea is that it takes
an input file with extension .myPpExt in the source directory and output a Haskell
source file in the output directory.
We defined a custom argument type MyPPInput for our code generator. This
argument type must be serialisable and have equality, so we derive
Eq and Binary. It would be fine to just use a tuple as well, but
a datatype is a bit more readable.
Next, we should search for all input files that need to be preprocessed, and register one pre-build rule for each such file:
{-# LANGUAGE StaticPointers #-}
preBuildRules :: PreBuildComponentInputs -> RulesM ()
preBuildRules pbci@(PreBuildComponentInputs { buildingWhat = what, localBuildInfo = lbi, targetInfo = tgt }) = do
let clbi = targetCLBI tgt
autogenDir = autogenComponentModulesDir lbi clbi
-- Scan the source directories for .ppExt files, registering one rule each.
inputFiles <- findAndMonitorSourceDirsFileExts pbci ("ppExt" NE.:| [])
for_ inputFiles $ \loc@(Location srcDir relPath) -> do
let baseName = dropExtension (getSymbolicPath relPath)
registerRule_ (fromString $ "myPP:" ++ baseName) $
staticRule
(mkCommand (static Dict) (static runMyPP) $
MyPPInput { ppVerbFlags = buildingWhatVerbosity what
, ppSrcDir = srcDir
, ppOutDir = autogenDir
, ppBaseName = baseName })
-- Inputs of the rule: the ".ppExt" file in the source tree.
[ FileDependency loc ]
-- Outputs of the rule: a corresponding ".hs" file, in the
-- directory for autogenerated modules.
( Location autogenDir (makeRelativePathEx (baseName <.> "hs")) NE.:| [] )
Here, each rule has a single dependency (on the input .ppExt file), and
produces a single output (the .hs file). It is perfectly possible to define
rules that don’t depend on any files on disk, or that only depend on other rules.
Note that a rule should never depend on a file generated by another rule; instead
you should declare a dependency on the rule directly (using RuleDependency
instead of FileDependency, with the RuleId returned by registerRule).
The StaticPointers extension is used to ensure that code generators are
static (and that the instances they need, such as Binary MyPPInput, are also
static). This is explained in the Haskell Tech Proposal,
but you don’t really need to know the justification to use the API.
The essential restriction is that any code enclosed by static cannot refer
to locally-bound variables. It is thus crucial that runMyPP is defined as a
top-level function, and any arguments it needs must be passed via its argument
type (in this case, the MyPPInput type).
Refer to the GHC user guide entry on Static Pointers
for more information.
In this example, all rule dependencies are static, so we used staticRule.
For rules with dynamic dependencies (e.g. parsing dependencies from the
input files), you can use dynamicRule. The API for this is more complex;
refer to the Hackage documentation
for details.
A fully worked out representative example usage of pre-build rules, for preprocessing LBNF grammars, can be found here.