diff --git a/Cabal-hooks/src/Distribution/Simple/SetupHooks.hs b/Cabal-hooks/src/Distribution/Simple/SetupHooks.hs index d7ac41f9817..688f4953e71 100644 --- a/Cabal-hooks/src/Distribution/Simple/SetupHooks.hs +++ b/Cabal-hooks/src/Distribution/Simple/SetupHooks.hs @@ -1,10 +1,13 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE StaticPointers #-} +{-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-} + {-| Module: Distribution.Simple.SetupHooks Description: Interface for the @Hooks@ @build-type@. @@ -16,7 +19,9 @@ 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 'SetupHooks' for more details, as well as the +[introductory blog post](https://well-typed.com/blog/2025/01/cabal-hooks) for +the feature. -} module Distribution.Simple.SetupHooks ( -- * Hooks @@ -25,6 +30,9 @@ module Distribution.Simple.SetupHooks SetupHooks(..) , noSetupHooks + -- * Usage overview + -- $usage + -- * Configure hooks -- $configureHooks @@ -66,33 +74,77 @@ module Distribution.Simple.SetupHooks , Rules , rules , noRules + + -- *** Rules API + + -- $rulesAPI + , RulesM + -- | Rule names (use @OverloadedStrings@ or 'Data.String.fromString') + , ShortText + , 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 + -- **** Path types and utilities + , RelativePath + , sameDirectory + , getSymbolicPath + , makeRelativePathEx + , moduleNameSymbolicPath + , FileLike(..) + , PathLike(..) + + -- ***** Directory types + , Source, Build, Pkg, CWD + + -- **** File search + , findAndMonitorDirFileGlob + , findAndMonitorSourceDirsFileExt + + -- ***** File globbing re-exports + , Glob(..) + , GlobPiece(..) + , GlobSyntaxError(..) + , parseFileGlob + , globMatches + , runDirFileGlob + -- *** Actions - , RuleCommands(..) + , RuleCommands -- gnarly constructors not exposed; API is via 'staticRule' and 'dynamicRule' , Command , mkCommand , Dict(..) + -- | Custom datatypes used as rule command arguments must implement + -- serialisation. Derive these instances using @DeriveAnyClass@ and + -- @DeriveGeneric@: + -- + -- > data MyInput = MyInput { .. } + -- > deriving stock ( Eq, Show, Generic ) + -- > deriving anyclass Binary + , Binary - -- *** Rules API - - -- $rulesAPI - , RulesM - , registerRule - , registerRule_ - -- **** File/directory monitoring + -- *** File/directory monitoring , addRuleMonitors , module Distribution.Simple.FileMonitor.Types @@ -125,10 +177,22 @@ module Distribution.Simple.SetupHooks , ProgramDb , addKnownPrograms , configureUnconfiguredProgram + , lookupProgram + , lookupProgramByName , simpleProgram + , runProgramCwd + + -- *** IO utilities + , warn + , createDirectoryIfMissingVerbose + , rewriteFileEx -- ** General @Cabal@ datatypes - , Verbosity, Compiler(..), Platform(..), Suffix(..) + , Compiler(..), Platform(..), Suffix(..) + + -- *** Verbosity + , Verbosity, VerbosityFlags, VerbosityHandles + , mkVerbosity, defaultVerbosityHandles -- *** Package information , LocalBuildConfig, LocalBuildInfo, PackageBuildDescr @@ -139,8 +203,16 @@ module Distribution.Simple.SetupHooks , PackageDescription(..) + -- **** LocalBuildInfo utilities + , localPkgDescr + , mbWorkDirLBI + , withPrograms + , interpretSymbolicPathLBI + , componentBuildInfo + -- *** Component information , Component(..), ComponentName(..), componentName + , ModuleName , BuildInfo(..), emptyBuildInfo , TargetInfo(..), ComponentLocalBuildInfo(..) @@ -153,6 +225,10 @@ module Distribution.Simple.SetupHooks ) where +import Distribution.Compat.Binary + ( Binary ) +import Distribution.ModuleName + ( ModuleName ) import Distribution.PackageDescription ( PackageDescription(..) , Library(..), ForeignLib(..) @@ -169,15 +245,28 @@ import Distribution.Simple.Compiler import Distribution.Simple.Errors ( CabalException(SetupHooksException) ) import Distribution.Simple.FileMonitor.Types + hiding ( Glob ) +import Distribution.Simple.Glob + ( Glob, GlobSyntaxError(..) + , globMatches, runDirFileGlob, parseFileGlob + ) +import Distribution.Simple.Glob.Internal + ( Glob(..), GlobPiece(..) + ) import Distribution.Simple.Install ( installFileGlob ) import Distribution.Simple.LocalBuildInfo - ( componentBuildDir ) + ( componentBuildDir, componentBuildInfo + , mbWorkDirLBI, interpretSymbolicPathLBI + ) import Distribution.Simple.PreProcess.Types ( Suffix(..) ) +import Distribution.Simple.Program + ( runProgramCwd ) import Distribution.Simple.Program.Db ( ProgramDb, addKnownPrograms , configureUnconfiguredProgram + , lookupProgram, lookupProgramByName ) import Distribution.Simple.Program.Find ( simpleProgram ) @@ -198,7 +287,11 @@ import Distribution.Simple.SetupHooks.Errors import Distribution.Simple.SetupHooks.Internal import Distribution.Simple.SetupHooks.Rule as Rule import Distribution.Simple.Utils - ( dieWithException ) + ( dieWithException + , warn + , createDirectoryIfMissingVerbose + , rewriteFileEx + ) import Distribution.System ( Platform(..) ) import Distribution.Types.Component @@ -206,15 +299,25 @@ import Distribution.Types.Component import Distribution.Types.ComponentLocalBuildInfo ( ComponentLocalBuildInfo(..) ) import Distribution.Types.LocalBuildInfo - ( LocalBuildInfo(..) ) + ( LocalBuildInfo(..), withPrograms ) import Distribution.Types.LocalBuildConfig ( LocalBuildConfig, PackageBuildDescr ) import Distribution.Types.TargetInfo ( TargetInfo(..) ) +import Distribution.Utils.Path + ( SymbolicPath, CWD, Pkg, FileOrDir(..) + , interpretSymbolicPath, makeRelativePathEx + , RelativePath, Source, Build + , getSymbolicPath, sameDirectory, moduleNameSymbolicPath + , FileLike(..), PathLike(..) + ) import Distribution.Utils.ShortText ( ShortText ) import Distribution.Verbosity - ( Verbosity ) + ( Verbosity + , VerbosityFlags, VerbosityHandles + , mkVerbosity, defaultVerbosityHandles + ) import Control.Monad ( void ) @@ -227,6 +330,8 @@ import qualified Control.Monad.Trans.State as State import qualified Control.Monad.Trans.Writer.CPS as Writer import Data.Foldable ( for_ ) +import Data.Traversable + ( for ) import Data.Map.Strict as Map ( insertLookupWithKey ) @@ -279,6 +384,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 + 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 @@ -314,18 +478,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). @@ -335,11 +505,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 @@ -398,7 +654,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] @@ -450,9 +707,61 @@ registerRule_ i r = void $ registerRule i r -- | Declare additional monitored objects for the collection of all rules. -- -- When these monitored objects change, the rules are re-computed. +-- +-- See also 'findAndMonitorDirFileGlob' which combines the search and the +-- monitoring. addRuleMonitors :: Monad m => [MonitorFilePath] -> RulesT m () addRuleMonitors = RulesT . lift . lift . Writer.tell {-# INLINEABLE addRuleMonitors #-} --- TODO: add API functions that search and declare the appropriate monitoring --- at the same time. +-- | Retrieve all files matching the given 'Glob' in the specified search +-- directories. +-- +-- See also the canned 'findAndMonitorSourceDirsFileExt' for the simple +-- case of monitoring a file extension in the source directories of a component. +findAndMonitorDirFileGlob + :: MonadIO m + => Maybe (SymbolicPath CWD (Dir Pkg)) + -> Verbosity + -> [SymbolicPath Pkg (Dir dir)] + -- ^ search directories + -> Glob + -- ^ pattern to match against + -> RulesT m [Location] +findAndMonitorDirFileGlob mbWorkDir verb searchDirs glob = do + matchingFiles <- fmap concat $ liftIO $ for searchDirs $ \srcDir -> do + let root = interpretSymbolicPath mbWorkDir srcDir + matches <- runDirFileGlob verb Nothing root glob + return + [ Location srcDir (makeRelativePathEx match) + | match <- globMatches matches + ] + addRuleMonitors [monitorFileGlobExistence $ RootedGlob FilePathRelative glob] + return matchingFiles +{-# INLINEABLE findAndMonitorDirFileGlob #-} + +-- | Scans the component source directories for files with the given extension, +-- and monitors the resulting file glob. +findAndMonitorSourceDirsFileExt + :: MonadIO m + => PreBuildComponentInputs + -> String -- ^ extension (not including the @.@) + -> RulesT m [Location] +findAndMonitorSourceDirsFileExt + PreBuildComponentInputs + { localBuildInfo = lbi + , buildingWhat = what + , targetInfo = tgt + } ext = + let + comp = targetComponent tgt + verbosity = mkVerbosity defaultVerbosityHandles (buildingWhatVerbosity what) + mbWorkDir = mbWorkDirLBI lbi + searchDirs = hsSourceDirs $ componentBuildInfo comp + glob = either (error . show) id $ + parseFileGlob + (specVersion $ localPkgDescr lbi) + ("**/*." ++ ext) + in + findAndMonitorDirFileGlob mbWorkDir verbosity searchDirs glob +{-# INLINEABLE findAndMonitorSourceDirsFileExt #-} diff --git a/Cabal/src/Distribution/Simple/SetupHooks/Rule.hs b/Cabal/src/Distribution/Simple/SetupHooks/Rule.hs index 1cee0fa14c8..b5dc929583c 100644 --- a/Cabal/src/Distribution/Simple/SetupHooks/Rule.hs +++ b/Cabal/src/Distribution/Simple/SetupHooks/Rule.hs @@ -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 @@ -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 @@ -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) diff --git a/changelog.d/UnexposeRuleCommands.md b/changelog.d/UnexposeRuleCommands.md new file mode 100644 index 00000000000..eab08b3c6bb --- /dev/null +++ b/changelog.d/UnexposeRuleCommands.md @@ -0,0 +1,10 @@ +--- +synopsis: Stop exposing constructors of RuleCommands +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`. diff --git a/changelog.d/hooks-api-exports.md b/changelog.d/hooks-api-exports.md new file mode 100644 index 00000000000..5c1e35da267 --- /dev/null +++ b/changelog.d/hooks-api-exports.md @@ -0,0 +1,24 @@ +--- +synopsis: Make Cabal-hooks library more self-sufficient +packages: [Cabal-hooks] +prs: 11772 +issues: +--- + +The `Distribution.Simple.SetupHooks` module from `Cabal-hooks` now re-exports +a lot of the functionality that is commonly needed when writing `SetupHooks`: + + - File-path related functionality from `Distribution.Utils.Path`. + - Functionality related to the program database: `lookupProgram`, `runProgramCwd`. + - IO utilities such as `warn`, `createDirectoryIfMissingVerbose`, and `rewriteFileEx`. + - Various types frequently used in pre-build rules, such as `Binary`, + `ModuleName`. + - Functions that extract information from `LocalBuildInfo` such as + `localPkgDescr`, `mbWorkDirLBI`, `withPrograms`, `interpretSymbolicPathLBI` + and `componentBuildInfo`. + + +In addition, new file monitoring helper functions `findAndMonitorDirFileGlob` +and `findAndMonitorSourceDirsFileExt` have been added. These make it very +simple and convenient to search for a file glob or files with a particular +extension in the source directories, for pre-build rules. diff --git a/doc/cabal-package-description-file.rst b/doc/cabal-package-description-file.rst index 9b73463a516..184c2abae56 100644 --- a/doc/cabal-package-description-file.rst +++ b/doc/cabal-package-description-file.rst @@ -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 `. Refer to the `Hackage documentation for the Distribution.Simple.SetupHooks module `__ -for an overview of the ``Hooks`` API. Further motivation and a technical overview -of the design is available in `Haskell Tech Proposal #60 `__ . +for a full reference of the ``Hooks`` API. Further motivation and a technical overview +of the design is available in `Haskell Tech Proposal #60 `__. .. _custom-setup: diff --git a/doc/how-to-use-setup-hooks.rst b/doc/how-to-use-setup-hooks.rst new file mode 100644 index 00000000000..326f0b01fde --- /dev/null +++ b/doc/how-to-use-setup-hooks.rst @@ -0,0 +1,273 @@ +.. _setup-hooks-guide: + +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 +:ref:`Custom build type `. + +See also: + + - `Hackage documentation for the Cabal-hooks package + `__ + - `Haskell Tech Proposal introducing the feature + `__ + +.. _setup-hooks-motivation: + +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``. + + +.. _setup-hooks-usage: + +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``: + +.. code-block:: cabal + + 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: + +.. code-block:: haskell + + 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. + +.. _setup-hooks-basic-hooks: + +Basic hooks +----------- + +With the exception of pre-build rules, all hooks take the form: + +.. code-block:: haskell + + HookInput -> IO HookOutput + +Specifying a hook thus amounts to providing a Haskell function of that type. + +For the package-wide pre-configure, we have: + +.. code-block:: haskell + + type PreConfPackageHook = PreConfPackageInputs -> IO PreConfPackageOutputs + +and a typical hook would look like: + +.. code-block:: haskell + + 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: + +.. code-block:: haskell + + type PreConfComponentHook = PreConfComponentInputs -> IO PreConfComponentOutputs + +and a typical hook would look like: + +.. code-block:: haskell + + 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: + +.. code-block:: haskell + + module SetupHooks ( setupHooks ) where + + import Distribution.Simple.SetupHooks + + setupHooks :: SetupHooks + setupHooks = + noSetupHooks + { configureHooks = noConfigureHooks + { preConfPackageHook = Just myPreConfPackageHook + , preConfComponentHook = Just myPreConfComponentHook + } + } + +.. _setup-hooks-pre-build-rules: + +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 ()`. + +.. code-block:: haskell + + {-# 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: + +.. code-block:: haskell + + {-# 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 <- findAndMonitorSourceDirsFileExt pbci "ppExt" + 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. diff --git a/doc/index.rst b/doc/index.rst index 4bd13c65d7a..d14e29674a0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ Welcome to the Cabal User Guide how-to-build-like-nix how-to-run-in-windows how-to-use-backpack + how-to-use-setup-hooks how-to-report-bugs .. toctree::