Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 184 additions & 18 deletions Cabal-hooks/src/Distribution/Simple/SetupHooks.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ a module @SetupHooks.hs@ which exports a value @setupHooks :: 'SetupHooks'@.
This is a record that declares actions that should be hooked into the
cabal build process.

See 'SetupHooks' for more details.
See also the introductory [how to guide](https://cabal.readthedocs.io/en/latest/how-to-use-setup-hooks.html)
in the Cabal user manual.
-}
module Distribution.Simple.SetupHooks
( -- * Hooks
Expand All @@ -25,6 +26,9 @@ module Distribution.Simple.SetupHooks
SetupHooks(..)
, noSetupHooks

-- * Usage overview
-- $usage

-- * Configure hooks

-- $configureHooks
Expand Down Expand Up @@ -66,33 +70,43 @@ module Distribution.Simple.SetupHooks
, Rules
, rules
, noRules

-- *** Rules API

-- $rulesAPI
, RulesM
, registerRule
, registerRule_

-- *** Rule construction
, Rule
, Dependency (..)
, RuleOutput (..)
, RuleId
, staticRule, dynamicRule

-- **** Example usage of 'dynamicRule'
-- $dynamicRules

-- *** Rule inputs/outputs

-- **** Rule dependencies
-- $rulesDemand
, Dependency (..)
, RuleOutput (..)
, RuleId

-- **** Filesystem locations
, Location(..)
, location
, autogenComponentModulesDir
, componentBuildDir

-- *** Actions
, RuleCommands(..)
, RuleCommands -- gnarly constructors not exposed; API is via 'staticRule' and 'dynamicRule'
, Command
, mkCommand
, Dict(..)

-- *** Rules API

-- $rulesAPI
, RulesM
, registerRule
, registerRule_

-- **** File/directory monitoring
-- *** File/directory monitoring
, addRuleMonitors
, module Distribution.Simple.FileMonitor.Types

Expand Down Expand Up @@ -279,6 +293,65 @@ Note that 'SetupHooks' can be monoidally combined, e.g.:
> mySetupHooks = ...
-}

{- $usage
'SetupHooks' allow hooks into the following phases:

* The configure phase, via 'ConfigureHooks'.

There are three hooks into the configure phase:

* Package-wide pre-configure hook.

This hook enables custom logic in the style of traditional @./configure@
scripts, e.g. finding out information about the system and configuring
dependencies.

* Package-wide post-configure hook.

This hook is mostly used to write write custom package-wide information
to disk so that the next step can read it without repeating work.

* Per-component pre-configure hooks.

These hooks are used to modify individual components, e.g. declaring
new modules (such as autogenerated modules whose module names are not
statically known) or specifying per-component flags to be used when
building each component.

You can think of this step as dynamically updating the stanza for a
single component in the .cabal file of a package.

* The build phase, via 'BuildHooks'.

There are two hooks into the build phase:

* Per-component pre-build rules.

A rule can be thought of an invocation of a code generator; each rule
specifies how to build a particular output.

You can have rules that don't depend on any inputs (e.g. directly
generate a collection of modules programmatically, perhaps from some
kind of parsed schema), as well as preprocessor-like rules that take in
input files, e.g. the Happy parser generator that takes in a .y
file and generates a corresponding .hs file.

This rather elaborate setup (compared to a one-shot program that
generates the required modules) allows proper recompilation checking.
See also <https://well-typed.com/blog/2025/01/cabal-hooks/#pre-build-rules>
for some worked examples of pre-build rules.

* Per-component post-build hooks.

These can be thought of as "pre-linking hooks" and allow injecting
additional data into the final executable.

* The install phase, via 'InstallHooks'.

There is a single, per-component install hook. This allows copying over
additional files when installing a component (library/executable).
-}

{- $configureHooks
Configure hooks can be used to augment the Cabal configure logic with
package-specific logic. The main principle is that the configure hooks can
Expand Down Expand Up @@ -314,18 +387,24 @@ Each t'Rule' consists of:
- a specification of its static dependencies and outputs,
- the commands that execute the rule.

You can think of a t'Rule' as describing a particular invocation of a code
generator or preprocessor, with the t'Command' specifying the particular
command that will be executed.

Rules are constructed using either one of the 'staticRule' or 'dynamicRule'
smart constructors. Directly constructing a t'Rule' using the constructors of
that data type is not advised, as this relies on internal implementation details
which are subject to change in between versions of the `Cabal-hooks` library.
which are subject to change in between versions of the @Cabal-hooks@ library.

Note that:

- To declare the dependency on the output of a rule, one must refer to the
rule directly, and not to the path to the output executing that rule will
eventually produce.
To do so, registering a t'Rule' with the API returns a unique identifier
for that rule, in the form of a t'RuleId'.

This is achieved by using the t'RuleId' returned by 'registerRule', which
is the unique identifier for that rule.

- File dependencies and outputs are not specified directly by
'FilePath', but rather use the 'Location' type (which is more convenient
when working with preprocessors).
Expand All @@ -335,11 +414,97 @@ Note that:
when to re-compute the entire set of rules.
-}

{- $dynamicRules
The 'dynamicRule' smart constructor allows specifying rules that have dynamic
dependencies. This is the most complex part of the API, so let's work through
a representative example.

Suppose for example that we have preprocessor for files that may depend on
each other via explicit import statements at the start of the file. To properly
preprocess these, we need two commands:

1. A "find dependencies" command that parses the header to find the dependencies.
2. A "run preprocessor" command. This command might need to be passed the
dependencies as arguments, so the command in (1) should make that information
available to (2).

This is exactly what 'dynamicRule' allows us to do. The first command is (1),
which returns dynamic dependencies and additional data passed to the second
command, (2).

The rules for such a preprocessor would thus look something like (sketch):

@
ppDynPreBuildRules :: PreBuildComponentInputs -> RulesM ()
ppDynPreBuildRules pbci = mdo -- NB: using RecursiveDo

-- Scan filesystem for files to preprocess, e.g. with a monitored file glob.
inputModNms <- ...

let

-- (1): the "find dependencies" action
computeDepsAction
:: (Map ModuleName RuleId, FilePath)
-> IO ([Dependency], [ModuleName])
computeDepsAction (modNmToRuleId, inFile) = do
src <- readFile inFile
let dynDeps = parseHeaderImports src
return
( [ RuleDependency $ RuleOutput rId 1
| dep <- dynDeps
, let rId = modNmToRuleId Map.! dep ]
, dynDeps )

-- (2): the "run preprocessor" action
runPPAction :: (FilePath, FilePath) -> [ModuleName] -> IO ()
runPPAction (inFile, outFile) dynDeps = do
src <- readFile inFile
let ppResult = preprocessWithDependencies dynDeps src
writeFile outFile ppResult

-- Construct a rule with dynamic dependencies using 'dynamicRule'
mkRule modNm =
dynamicRule (static Dict)
(mkCommand (static Dict) (static computeDepsAction) (allRuleIDs, inFile))
(mkCommand (static Dict) (static runPPAction) (inFile, outFile))
[ FileDependency $ Location sameDirectory (modPath <.> "myPp") ]
( Location autogenDir (modPath <.> "hs" )
NE.:| [ Location buildDir (unsafeCoerceSymbolicPath modPath <.> "myPpIface") ] )
where
modPath = moduleNameSymbolicPath modNm
inFile = getSymbolicPath (sameDirectory </> modPath <.> "myPp")
outFile = getSymbolicPath (autogenDir </> modPath <.> "hs")

autogenDir = ... -- derived from pbci
buildDir = ... -- derived from pbci

registerOne :: ModuleName -> RulesM (ModuleName, RuleId)
registerOne modNm = do
rId <- registerRule ("MyPP: " <> show modNm) (mkRule modNm)
return (modNm, rId)

-- Return a map from ModuleName to RuleId (used above via RecursiveDo).
allRuleIDs <- Map.fromList <$> traverse registerOne inputModNms
return ()
@

Note the usage of @RecursiveDo@ to allow indexing into the set of all registered
rules in order to declare the dynamic dependencies of the rules.

In this example we use tuples for the command arguments and @[ModuleName]@ for
the result of the dynamic dependency command; these have the required instances
needed for serialisation. If you use custom datatypes for these, you will need
to derive @Binary@, @Show@, @Eq@ to satisfy the API requirements (enforced by
the various calls to @static Dict@).

-}

{- $rulesDemand
Rules can declare various kinds of dependencies:

- 'staticDependencies': files or other rules that a rule statically depends on,
- extra dynamic dependencies, using the 'DynamicRuleCommands' constructor,
- static dependencies: files or other rules that a rule statically depends on,
- extra dynamic dependencies, using 'dynamicRule' smart constructor,
- 'MonitorFilePath': additional files and directories to monitor.

Rules are considered __out-of-date__ precisely when any of the following
Expand Down Expand Up @@ -398,7 +563,8 @@ datatypes, respectively.
We use 'addRuleMonitors' to declare a monitored directory that the collection
of rules as a whole depends on. In this case, we declare that they depend on the
contents of the "searchDir" directory. This means that the rules will be
computed anew whenever the contents of this directory change.
computed anew whenever the contents of this directory change. (This does not
mean all the rules will be re-run; only the out-of-date rules will be re-run.)
-}

{- Note [Not hiding SetupHooks constructors]
Expand Down
25 changes: 24 additions & 1 deletion Cabal/src/Distribution/Simple/SetupHooks/Rule.hs
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,17 @@ instance Show RuleBinary where
-- | A rule with static dependencies.
--
-- Prefer using this smart constructor instead of v'Rule' whenever possible.
--
-- See also 'dynamicRule' which adds support for dynamic dependencies.
staticRule
:: forall arg
. Typeable arg
=> Command arg (IO ())
-- ^ command to execute the rule
-> [Dependency]
-- ^ static dependencies of the rule
-> NE.NonEmpty Location
-- ^ rule results
-> Rule
staticRule cmd dep res =
Rule
Expand All @@ -307,17 +312,32 @@ staticRule cmd dep res =
, results = res
}

-- | A rule with dynamic dependencies.
-- | A rule with dynamic dependencies, which consists of two parts:
--
-- - a dynamic dependency computation, that returns additional edges to
-- be added to the build graph, together with an additional piece of data,
-- - the command to execute the rule itself, which receives the additional
-- piece of data returned by the dependency computation.
--
-- Prefer using this smart constructor instead of v'Rule' whenever possible.
--
-- Use 'staticRule' if you do not have any dynamic dependencies.
dynamicRule
:: forall depsArg depsRes arg
. (Typeable depsArg, Typeable depsRes, Typeable arg)
=> StaticPtr (Dict (Binary depsRes, Show depsRes, Eq depsRes))
-- ^ evidence that the result of the dynamic dependency command
-- is serialisable
-> Command depsArg (IO ([Dependency], depsRes))
-- ^ dynamic dependency computation, returning dynamic dependencies
-- and an additional piece of data to be consumed by the main rule command
-> Command arg (depsRes -> IO ())
-- ^ main rule command; takes in the piece of data returned by the dyn-deps
-- command
-> [Dependency]
-- ^ static dependencies of the rule
-> NE.NonEmpty Location
-- ^ rule results
-> Rule
dynamicRule dict depsCmd action dep res =
Rule
Expand Down Expand Up @@ -624,6 +644,9 @@ runCommand (Command{actionPtr = UserStatic ptr, actionArg = ScopedArgument arg})
-- - for a rule with static dependencies, a single command,
-- - for a rule with dynamic dependencies, a command for computing dynamic
-- dependencies, and a command for executing the rule.
--
-- Prefer using 'staticRule' and 'dynamicRule' instead of the (internal)
-- constructors of 'RuleCommands'.
data
RuleCommands
(scope :: Scope)
Expand Down
10 changes: 10 additions & 0 deletions changelog.d/UnexposeRuleCommands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
synopsis: Stop exposing constructors of RuleCommands
Comment thread
sheaf marked this conversation as resolved.
packages: [Cabal-hooks]
prs: 11771
issues: 11461
---

The constructors of the `SetupHooks` `RuleCommands` are no longer exposed from
`Distribution.Simple.SetupHooks`. These were rather gnarly internal constructors;
the intended public API is via `staticRule` and `dynamicRule`.
6 changes: 4 additions & 2 deletions doc/cabal-package-description-file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2893,9 +2893,11 @@ while a basic ``SetupHooks.hs`` file might look like the following:

-- ...

For a detailed guide on using the ``Hooks`` build type, see
:ref:`How to use the Hooks build type <setup-hooks-guide>`.
Refer to the `Hackage documentation for the Distribution.Simple.SetupHooks module <https://hackage.haskell.org/package/Cabal-hooks/docs/Distribution-Simple-SetupHooks.html>`__
for an overview of the ``Hooks`` API. Further motivation and a technical overview
of the design is available in `Haskell Tech Proposal #60 <https://github.com/haskellfoundation/tech-proposals/blob/main/rfc/060-replacing-cabal-custom-build.md>`__ .
for a full reference of the ``Hooks`` API. Further motivation and a technical overview
of the design is available in `Haskell Tech Proposal #60 <https://github.com/haskellfoundation/tech-proposals/blob/main/rfc/060-replacing-cabal-custom-build.md>`__.

.. _custom-setup:

Expand Down
Loading
Loading