{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StaticPointers #-}

{-|
Module: Distribution.Simple.SetupHooks
Description: Interface for the @Hooks@ @build-type@.

This module defines the interface for the @Hooks@ @build-type@.

To write a package that implements @build-type: Hooks@, you should define
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.
-}
module Distribution.Simple.SetupHooks
  ( -- * Hooks

    -- $setupHooks
    SetupHooks(..)
  , noSetupHooks

     -- * Configure hooks

     -- $configureHooks
  , ConfigureHooks(..)
  , noConfigureHooks
    -- ** Per-package configure hooks
  , PreConfPackageInputs(..)
  , PreConfPackageOutputs(..) -- See Note [Not hiding SetupHooks constructors]
  , noPreConfPackageOutputs
  , PreConfPackageHook
  , PostConfPackageInputs(..)
  , PostConfPackageHook
    -- ** Per-component configure hooks
  , PreConfComponentInputs(..)
  , PreConfComponentOutputs(..) -- See Note [Not hiding SetupHooks constructors]
  , noPreConfComponentOutputs
  , PreConfComponentHook
  , ComponentDiff(..), emptyComponentDiff, buildInfoComponentDiff
  , LibraryDiff, ForeignLibDiff, ExecutableDiff
  , TestSuiteDiff, BenchmarkDiff
  , BuildInfoDiff

    -- * Build hooks

  , BuildHooks(..), noBuildHooks
  , BuildingWhat(..), buildingWhatVerbosity, buildingWhatDistPref

    -- ** Pre-build rules

    -- $preBuildRules
  , PreBuildComponentInputs(..)
  , PreBuildComponentRules

    -- ** Post-build hooks
  , PostBuildComponentInputs(..)
  , PostBuildComponentHook

    -- ** Rules
  , Rules
  , rules
  , noRules
  , Rule
  , Dependency (..)
  , RuleOutput (..)
  , RuleId
  , staticRule, dynamicRule
    -- *** Rule inputs/outputs

    -- $rulesDemand
  , Location(..)
  , location
  , autogenComponentModulesDir
  , componentBuildDir

    -- *** Actions
  , RuleCommands(..)
  , Command
  , mkCommand
  , Dict(..)

    -- *** Rules API

    -- $rulesAPI
  , RulesM
  , registerRule
  , registerRule_

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

    -- * Install hooks
  , InstallHooks(..), noInstallHooks
  , InstallComponentInputs(..), InstallComponentHook

    -- * Re-exports

    -- ** Hooks
    -- *** Configure hooks
  , ConfigFlags(..)
    -- *** Build hooks
  , BuildFlags(..), ReplFlags(..), HaddockFlags(..), HscolourFlags(..)
    -- *** Install hooks
  , CopyFlags(..)

    -- ** @Hooks@ API
    --
    -- | These are functions provided as part of the @Hooks@ API.
    -- It is recommended to import them from this module as opposed to
    -- manually importing them from inside the Cabal module hierarchy.

    -- *** Copy/install functions
  , installFileGlob

    -- *** Interacting with the program database
  , Program(..), ConfiguredProgram(..), ProgArg
  , ProgramLocation(..)
  , ProgramDb
  , addKnownPrograms
  , configureUnconfiguredProgram
  , simpleProgram

    -- ** General @Cabal@ datatypes
  , Verbosity, Compiler(..), Platform(..), Suffix(..)

    -- *** Package information
  , LocalBuildConfig, LocalBuildInfo, PackageBuildDescr
      -- NB: we can't simply re-export all the fields of LocalBuildConfig etc,
      -- due to the presence of duplicate record fields.
      -- Ideally, we'd like to e.g. re-export LocalBuildConfig qualified,
      -- but qualified re-exports aren't a thing currently.

  , PackageDescription(..)

    -- *** Component information
  , Component(..), ComponentName(..), componentName
  , BuildInfo(..), emptyBuildInfo
  , TargetInfo(..), ComponentLocalBuildInfo(..)

    -- **** Components
  , Library(..), ForeignLib(..), Executable(..)
  , TestSuite(..), Benchmark(..)
  , LibraryName(..)
  , emptyLibrary, emptyForeignLib, emptyExecutable
  , emptyTestSuite, emptyBenchmark

  )
where
import Distribution.PackageDescription
  ( PackageDescription(..)
  , Library(..), ForeignLib(..)
  , Executable(..), TestSuite(..), Benchmark(..)
  , emptyLibrary, emptyForeignLib
  , emptyExecutable, emptyBenchmark, emptyTestSuite
  , BuildInfo(..), emptyBuildInfo
  , ComponentName(..), LibraryName(..)
  )
import Distribution.Simple.BuildPaths
  ( autogenComponentModulesDir )
import Distribution.Simple.Compiler
  ( Compiler(..) )
import Distribution.Simple.Errors
  ( CabalException(SetupHooksException) )
import Distribution.Simple.FileMonitor.Types
import Distribution.Simple.Install
  ( installFileGlob )
import Distribution.Simple.LocalBuildInfo
  ( componentBuildDir )
import Distribution.Simple.PreProcess.Types
  ( Suffix(..) )
import Distribution.Simple.Program.Db
  ( ProgramDb, addKnownPrograms
  , configureUnconfiguredProgram
  )
import Distribution.Simple.Program.Find
  ( simpleProgram )
import Distribution.Simple.Program.Types
  ( Program(..), ConfiguredProgram(..)
  , ProgArg
  , ProgramLocation(..)
  )
import Distribution.Simple.Setup
  ( BuildFlags(..)
  , ConfigFlags(..)
  , CopyFlags(..)
  , HaddockFlags(..)
  , HscolourFlags(..)
  , ReplFlags(..)
  )
import Distribution.Simple.SetupHooks.Errors
import Distribution.Simple.SetupHooks.Internal
import Distribution.Simple.SetupHooks.Rule as Rule
import Distribution.Simple.Utils
  ( dieWithException )
import Distribution.System
  ( Platform(..) )
import Distribution.Types.Component
  ( Component(..), componentName )
import Distribution.Types.ComponentLocalBuildInfo
  ( ComponentLocalBuildInfo(..) )
import Distribution.Types.LocalBuildInfo
  ( LocalBuildInfo(..) )
import Distribution.Types.LocalBuildConfig
  ( LocalBuildConfig, PackageBuildDescr )
import Distribution.Types.TargetInfo
  ( TargetInfo(..) )
import Distribution.Utils.ShortText
  ( ShortText )
import Distribution.Verbosity
  ( Verbosity )

import Control.Monad
  ( void )
import Control.Monad.IO.Class
  ( MonadIO(liftIO) )
import Control.Monad.Trans.Class
  ( lift )
import qualified Control.Monad.Trans.Reader as Reader
import qualified Control.Monad.Trans.State as State
#if MIN_VERSION_transformers(0,5,6)
import qualified Control.Monad.Trans.Writer.CPS as Writer
#else
import qualified Control.Monad.Trans.Writer.Strict as Writer
#endif
import Data.Foldable
  ( for_ )
import Data.Map.Strict as Map
  ( insertLookupWithKey )

--------------------------------------------------------------------------------
-- Haddocks for the SetupHooks API

{- $setupHooks
A Cabal package with @Hooks@ @build-type@ must define the Haskell module
@SetupHooks@ which defines a value @setupHooks :: 'SetupHooks'@.

These *setup hooks* allow package authors to customise the configuration and
building of a package by providing certain hooks that get folded into the
general package configuration and building logic within @Cabal@.

This mechanism replaces the @Custom@ @build-type@, providing better
integration with the rest of the Haskell ecosystem.

Usage example:

> -- In your .cabal file
> build-type: Hooks
>
> custom-setup
>   setup-depends:
>     base        >= 4.18 && < 5,
>     Cabal-hooks >= 3.14 && < 3.15
>
> The declared Cabal version should also be at least 3.14.

> -- In SetupHooks.hs, next to your .cabal file
> module SetupHooks where
> import Distribution.Simple.SetupHooks ( SetupHooks, noSetupHooks )
>
> setupHooks :: SetupHooks
> setupHooks =
>  noSetupHooks
>    { configureHooks = myConfigureHooks
>    , buildHooks = myBuildHooks }

Note that 'SetupHooks' can be monoidally combined, e.g.:

> module SetupHooks where
> import Distribution.Simple.SetupHooks
> import qualified SomeOtherLibrary ( setupHooks )
>
> setupHooks :: SetupHooks
> setupHooks = SomeOtherLibrary.setupHooks <> mySetupHooks
>
> mySetupHooks :: SetupHooks
> mySetupHooks = ...
-}

{- $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
feed into updating the 'PackageDescription' of a @cabal@ package. From then on,
this package configuration is set in stone, and later hooks (e.g. hooks into
the build phase) can no longer modify this configuration; instead they will
receive this configuration in their inputs, and must honour it.

Configuration happens at two levels:

  * global configuration covers the entire package,
  * local configuration covers a single component.

Once the global package configuration is done, all hooks work on a
per-component level. The configuration hooks thus follow a simple philosophy:

  * All modifications to global package options must use `preConfPackageHook`.
  * All modifications to component configuration options must use `preConfComponentHook`.

For example, to generate modules inside a given component, you should:

  * In the per-component configure hook, declare the modules you are going to
    generate by adding them to the `autogenModules` field for that component
    (unless you know them ahead of time, in which case they can be listed
    textually in the @.cabal@ file of the project).
  * In the build hooks, describe the actions that will generate these modules.
-}

{- $preBuildRules
Pre-build hooks are specified as a collection of pre-build 'Rules'.
Each t'Rule' consists of:

  - a specification of its static dependencies and outputs,
  - the commands that execute the rule.

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.

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'.
  - File dependencies and outputs are not specified directly by
    'FilePath', but rather use the 'Location' type (which is more convenient
    when working with preprocessors).
  - Rules refer to the actions that execute them using static pointers, in order
    to enable serialisation/deserialisation of rules.
  - Rules can additionally monitor files or directories, which determines
    when to re-compute the entire set of rules.
-}

{- $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,
  - 'MonitorFilePath': additional files and directories to monitor.

Rules are considered __out-of-date__ precisely when any of the following
conditions apply:

  [O1] there has been a (relevant) change in the files and directories
       monitored by the rules,
  [O2] the environment passed to the computation of rules has changed.

If the rules are out-of-date, the build system is expected to re-run the
computation that computes all rules.

After this re-computation of the set of all rules, we match up new rules
with old rules, by 'RuleId'. A rule is then considered __stale__ if any of
following conditions apply:

  [N] the rule is new, or
  [S] the rule matches with an old rule, and either:

    [S1] a file dependency of the rule has been modified/created/deleted, or
         a (transitive) rule dependency of the rule is itself stale, or
    [S2] the rule is different from the old rule, e.g. the argument stored in
         the rule command has changed, or the pointer to the action to run the
         rule has changed. (This is determined using the @Eq Rule@ instance.)

A stale rule becomes no longer stale once we run its associated action. The
build system is responsible for re-running the actions associated with
each stale rule, in dependency order. This means the build system is expected
to behave as follows:

  1. Any time the rules are out-of-date, query the rules to obtain
     up-to-date rules.
  2. Re-run stale rules.
-}

{- $rulesAPI
Defining pre-build rules can be done in the following style:

> {-# LANGUAGE BlockArguments, StaticPointers #-}
> myPreBuildRules :: PreBuildComponentRules
> myPreBuildRules = rules (static ()) $ \ preBuildEnvironment -> do
>   let cmd1 = mkCommand (static Dict) $ static \ arg -> do { .. }
>       cmd2 = mkCommand (static Dict) $ static \ arg -> do { .. }
>   myData <- liftIO someIOAction
>   addRuleMonitors [ monitorDirectory "someSearchDir" ]
>   registerRule_ "rule_1_1" $ staticRule (cmd1 arg1) deps1 outs1
>   registerRule_ "rule_1_2" $ staticRule (cmd1 arg2) deps2 outs2
>   registerRule_ "rule_1_3" $ staticRule (cmd1 arg3) deps3 outs3
>   registerRule_ "rule_2_4" $ staticRule (cmd2 arg4) deps4 outs4

Here we use the 'rules', 'staticRule' and 'mkCommand' smart constructors,
rather than directly using the v'Rules', v'Rule' and v'Command' constructors,
which insulates us from internal changes to the t'Rules', t'Rule' and t'Command'
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.
-}

{- Note [Not hiding SetupHooks constructors]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We would like to hide as many datatype constructors from the API as possible
and provide smart constructors instead, so that hook authors don't end up
depending on internal implementation details that are subject to change.

However, doing so significantly degrades the Haddock documentation. So we
instead opt for exposing the constructor, but suggesting users use the
corresponding smart constructor instead.
-}

--------------------------------------------------------------------------------
-- API functions

-- | Register a rule. Returns an identifier for that rule.
registerRule
  :: ShortText -- ^ user-given rule name;
               -- these should be unique on a per-package level
  -> Rule      -- ^ the rule to register
  -> RulesM RuleId
registerRule nm !newRule = RulesT $ do
  RulesEnv { rulesEnvNameSpace = ns
           , rulesEnvVerbosity = verbosity } <- Reader.ask
  oldRules <- lift $ State.get
  let rId = RuleId { ruleNameSpace = ns, ruleName = nm }
      (mbDup, newRules) = Map.insertLookupWithKey (\ _ new _old -> new) rId newRule oldRules
  for_ mbDup $ \ oldRule ->
    liftIO $ dieWithException verbosity
           $ SetupHooksException
           $ RulesException
           $ DuplicateRuleId rId oldRule newRule
  lift $ State.put newRules
  return rId

-- | Register a rule, discarding the produced 'RuleId'.
--
-- Using this function means that you don't expect any other rules to ever
-- depend on any outputs of this rule. Use 'registerRule' to retain the
-- 'RuleId' instead.
registerRule_
  :: ShortText -- ^ user-given rule name;
               -- these should be unique on a per-package level
  -> Rule      -- ^ the rule to register
  -> RulesT IO ()
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.
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.
