{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}

-- |
-- Module: Distribution.Simple.SetupHooks.Internal
--
-- Internal implementation module.
-- Users of @build-type: Hooks@ should import "Distribution.Simple.SetupHooks"
-- instead.
module Distribution.Simple.SetupHooks.Internal
  ( -- * The setup hooks datatype
    SetupHooks (..)
  , noSetupHooks

    -- * Configure hooks
  , ConfigureHooks (..)
  , noConfigureHooks

    -- ** Per-package configure hooks
  , PreConfPackageInputs (..)
  , PreConfPackageOutputs (..)
  , noPreConfPackageOutputs
  , PreConfPackageHook
  , PostConfPackageInputs (..)
  , PostConfPackageHook

    -- ** Per-component configure hooks
  , PreConfComponentInputs (..)
  , PreConfComponentOutputs (..)
  , noPreConfComponentOutputs
  , PreConfComponentHook
  , ComponentDiff (..)
  , emptyComponentDiff
  , buildInfoComponentDiff
  , LibraryDiff
  , ForeignLibDiff
  , ExecutableDiff
  , TestSuiteDiff
  , BenchmarkDiff
  , BuildInfoDiff

    -- * Build hooks
  , BuildHooks (..)
  , noBuildHooks
  , BuildingWhat (..)
  , buildingWhatVerbosity
  , buildingWhatWorkingDir
  , buildingWhatDistPref

    -- ** Pre-build rules
  , PreBuildComponentInputs (..)
  , PreBuildComponentRules

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

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

    -- * Internals

    -- ** Per-component hook utilities
  , applyComponentDiffs
  , forComponents_

    -- ** Executing build rules
  , executeRules

    -- ** HookedBuildInfo compatibility code
  , hookedBuildInfoComponents
  , hookedBuildInfoComponentDiff_maybe
  )
where

import Distribution.Compat.Prelude
import Prelude ()

import Distribution.Compat.Lens ((.~))
import Distribution.PackageDescription
import Distribution.Simple.BuildPaths
import Distribution.Simple.Compiler (Compiler (..))
import Distribution.Simple.Errors
import Distribution.Simple.LocalBuildInfo
import Distribution.Simple.Program.Db
import Distribution.Simple.Setup
  ( BuildingWhat (..)
  , buildingWhatDistPref
  , buildingWhatVerbosity
  , buildingWhatWorkingDir
  )
import Distribution.Simple.Setup.Build (BuildFlags (..))
import Distribution.Simple.Setup.Config (ConfigFlags (..))
import Distribution.Simple.Setup.Copy (CopyFlags (..))
import Distribution.Simple.SetupHooks.Errors
import Distribution.Simple.SetupHooks.Rule
import qualified Distribution.Simple.SetupHooks.Rule as Rule
import Distribution.Simple.Utils
import Distribution.System (Platform (..))
import Distribution.Utils.Path

import qualified Distribution.Types.BuildInfo.Lens as BI (buildInfo)
import Distribution.Types.LocalBuildConfig as LBC
import Distribution.Types.TargetInfo
import Distribution.Verbosity

import qualified Data.ByteString.Lazy as LBS
import Data.Coerce (coerce)
import qualified Data.Graph as Graph
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import qualified Data.Set as Set

import System.Directory (doesFileExist)

--------------------------------------------------------------------------------
-- SetupHooks

-- | Hooks into the @cabal@ build phases.
--
-- Usage:
--
--  - In your @.cabal@ file, declare @build-type: Hooks@
--    (with a @cabal-version@ greater than or equal to @3.14@),
--  - In your @.cabal@ file, include a @custom-setup@ stanza
--    which declares the dependencies of your @SetupHooks@ module;
--    this will usually contain a dependency on the @Cabal-hooks@ package.
--  - Provide a @SetupHooks.hs@ module next to your @.cabal@ file;
--    it must export @setupHooks :: SetupHooks@.
data SetupHooks = SetupHooks
  { configureHooks :: ConfigureHooks
  -- ^ Hooks into the configure phase.
  , buildHooks :: BuildHooks
  -- ^ Hooks into the build phase.
  --
  -- These hooks are relevant to any build-like phase,
  -- such as repl or haddock.
  , installHooks :: InstallHooks
  -- ^ Hooks into the copy/install phase.
  }

-- | 'SetupHooks' can be combined monoidally. This is useful to combine
-- setup hooks defined by another package with your own package-specific
-- hooks.
--
-- __Warning__: this 'Semigroup' instance is not commutative.
instance Semigroup SetupHooks where
  SetupHooks
    { configureHooks = conf1
    , buildHooks = build1
    , installHooks = inst1
    }
    <> SetupHooks
      { configureHooks = conf2
      , buildHooks = build2
      , installHooks = inst2
      } =
      SetupHooks
        { configureHooks = conf1 <> conf2
        , buildHooks = build1 <> build2
        , installHooks = inst1 <> inst2
        }

instance Monoid SetupHooks where
  mempty = noSetupHooks

-- | Empty hooks.
noSetupHooks :: SetupHooks
noSetupHooks =
  SetupHooks
    { configureHooks = noConfigureHooks
    , buildHooks = noBuildHooks
    , installHooks = noInstallHooks
    }

--------------------------------------------------------------------------------
-- Configure hooks.

type PreConfPackageHook = PreConfPackageInputs -> IO PreConfPackageOutputs

-- | Inputs to the package-wide pre-configure step.
data PreConfPackageInputs = PreConfPackageInputs
  { configFlags :: ConfigFlags
  , localBuildConfig :: LocalBuildConfig
  -- ^ Warning: the 'ProgramDb' in the 'withPrograms' field
  -- will not contain any unconfigured programs.
  , compiler :: Compiler
  , platform :: Platform
  }
  deriving (Generic, Show)

-- | Outputs of the package-wide pre-configure step.
--
-- Prefer using 'noPreConfPackageOutputs' and overriding the fields
-- you care about, to avoid depending on implementation details
-- of this datatype.
data PreConfPackageOutputs = PreConfPackageOutputs
  { buildOptions :: BuildOptions
  , extraConfiguredProgs :: ConfiguredProgs
  }
  deriving (Generic, Show)

-- | Use this smart constructor to declare an empty set of changes
-- by the package-wide pre-configure hook, and override the fields you
-- care about.
--
-- Use this rather than v'PreConfPackageOutputs' to avoid relying on
-- internal implementation details of the latter.
noPreConfPackageOutputs :: PreConfPackageInputs -> PreConfPackageOutputs
noPreConfPackageOutputs (PreConfPackageInputs{localBuildConfig = lbc}) =
  PreConfPackageOutputs
    { buildOptions = LBC.withBuildOptions lbc
    , extraConfiguredProgs = Map.empty
    }

-- | Package-wide post-configure step.
--
-- Perform side effects. Last opportunity for any package-wide logic;
-- any subsequent hooks work per-component.
type PostConfPackageHook = PostConfPackageInputs -> IO ()

-- | Inputs to the package-wide post-configure step.
data PostConfPackageInputs = PostConfPackageInputs
  { localBuildConfig :: LocalBuildConfig
  , packageBuildDescr :: PackageBuildDescr
  }
  deriving (Generic, Show)

-- | Per-component pre-configure step.
--
-- For each component of the package, this hook can perform side effects,
-- and return a diff to the passed in component, e.g. to declare additional
-- autogenerated modules.
type PreConfComponentHook = PreConfComponentInputs -> IO PreConfComponentOutputs

-- | Inputs to the per-component pre-configure step.
data PreConfComponentInputs = PreConfComponentInputs
  { localBuildConfig :: LocalBuildConfig
  , packageBuildDescr :: PackageBuildDescr
  , component :: Component
  }
  deriving (Generic, Show)

-- | Outputs of the per-component pre-configure step.
--
-- Prefer using 'noPreComponentOutputs' and overriding the fields
-- you care about, to avoid depending on implementation details
-- of this datatype.
data PreConfComponentOutputs = PreConfComponentOutputs
  { componentDiff :: ComponentDiff
  }
  deriving (Generic, Show)

-- | Use this smart constructor to declare an empty set of changes
-- by a per-component pre-configure hook, and override the fields you
-- care about.
--
-- Use this rather than v'PreConfComponentOutputs' to avoid relying on
-- internal implementation details of the latter.
noPreConfComponentOutputs :: PreConfComponentInputs -> PreConfComponentOutputs
noPreConfComponentOutputs (PreConfComponentInputs{component = comp}) =
  PreConfComponentOutputs
    { componentDiff = emptyComponentDiff (componentName comp)
    }

-- | Configure-time hooks.
--
-- Order of execution:
--
--  - 'preConfPackageHook',
--  - configure the package,
--  - 'postConfPackageHook',
--  - 'preConfComponentHook',
--  - configure the components.
data ConfigureHooks = ConfigureHooks
  { preConfPackageHook :: Maybe PreConfPackageHook
  -- ^ Package-wide pre-configure hook. See 'PreConfPackageHook'.
  , postConfPackageHook :: Maybe PostConfPackageHook
  -- ^ Package-wide post-configure hook. See 'PostConfPackageHook'.
  , preConfComponentHook :: Maybe PreConfComponentHook
  -- ^ Per-component pre-configure hook. See 'PreConfComponentHook'.
  }

-- Note: these configure hooks don't track any kind of dependency information,
-- so we won't know when the configuration is out of date and should be re-done.
-- This seems okay: it should only matter while developing the package, in which
-- case it seems acceptable to rely on the user re-configuring.

instance Semigroup ConfigureHooks where
  ConfigureHooks
    { preConfPackageHook = prePkg1
    , postConfPackageHook = postPkg1
    , preConfComponentHook = preComp1
    }
    <> ConfigureHooks
      { preConfPackageHook = prePkg2
      , postConfPackageHook = postPkg2
      , preConfComponentHook = preComp2
      } =
      ConfigureHooks
        { preConfPackageHook =
            coerce
              ((<>) @(Maybe PreConfPkgSemigroup))
              prePkg1
              prePkg2
        , postConfPackageHook =
            postPkg1 <> postPkg2
        , preConfComponentHook =
            coerce
              ((<>) @(Maybe PreConfComponentSemigroup))
              preComp1
              preComp2
        }

instance Monoid ConfigureHooks where
  mempty = noConfigureHooks

-- | Empty configure phase hooks.
noConfigureHooks :: ConfigureHooks
noConfigureHooks =
  ConfigureHooks
    { preConfPackageHook = Nothing
    , postConfPackageHook = Nothing
    , preConfComponentHook = Nothing
    }

-- | A newtype to hang off the @Semigroup PreConfPackageHook@ instance.
newtype PreConfPkgSemigroup = PreConfPkgSemigroup PreConfPackageHook

instance Semigroup PreConfPkgSemigroup where
  PreConfPkgSemigroup f1 <> PreConfPkgSemigroup f2 =
    PreConfPkgSemigroup $
      \inputs@( PreConfPackageInputs
                  { configFlags = cfg
                  , compiler = comp
                  , platform = plat
                  , localBuildConfig = lbc0
                  }
                ) ->
          do
            PreConfPackageOutputs
              { buildOptions = opts1
              , extraConfiguredProgs = progs1
              } <-
              f1 inputs
            PreConfPackageOutputs
              { buildOptions = opts2
              , extraConfiguredProgs = progs2
              } <-
              f2 $
                PreConfPackageInputs
                  { configFlags = cfg
                  , compiler = comp
                  , platform = plat
                  , localBuildConfig =
                      lbc0
                        { LBC.withPrograms =
                            updateConfiguredProgs (`Map.union` progs1) $
                              LBC.withPrograms lbc0
                        , LBC.withBuildOptions = opts1
                        }
                  }
            return $
              PreConfPackageOutputs
                { buildOptions = opts2
                , extraConfiguredProgs = progs1 <> progs2
                }

-- | A newtype to hang off the @Semigroup PreConfComponentHook@ instance.
newtype PreConfComponentSemigroup = PreConfComponentSemigroup PreConfComponentHook

instance Semigroup PreConfComponentSemigroup where
  PreConfComponentSemigroup f1 <> PreConfComponentSemigroup f2 =
    PreConfComponentSemigroup $ \inputs ->
      do
        PreConfComponentOutputs
          { componentDiff = diff1
          } <-
          f1 inputs
        PreConfComponentOutputs
          { componentDiff = diff2
          } <-
          f2 inputs
        return $
          PreConfComponentOutputs
            { componentDiff = diff1 <> diff2
            }

--------------------------------------------------------------------------------
-- Build setup hooks.

data PreBuildComponentInputs = PreBuildComponentInputs
  { buildingWhat :: BuildingWhat
  -- ^ what kind of build phase are we hooking into?
  , localBuildInfo :: LocalBuildInfo
  -- ^ information about the package
  , targetInfo :: TargetInfo
  -- ^ information about an individual component
  }
  deriving (Generic, Show)

type PreBuildComponentRules = Rules PreBuildComponentInputs

data PostBuildComponentInputs = PostBuildComponentInputs
  { buildFlags :: BuildFlags
  , localBuildInfo :: LocalBuildInfo
  , targetInfo :: TargetInfo
  }
  deriving (Generic, Show)

type PostBuildComponentHook = PostBuildComponentInputs -> IO ()

-- | Build-time hooks.
data BuildHooks = BuildHooks
  { preBuildComponentRules :: Maybe PreBuildComponentRules
  -- ^ Per-component fine-grained pre-build rules.
  , postBuildComponentHook :: Maybe PostBuildComponentHook
  -- ^ Per-component post-build hook.
  }

-- Note that the pre-build hook consists of a function which takes a component
-- as an argument (as part of the targetInfo field) and returns a collection of
-- pre-build rules.
--
-- One might wonder why it isn't instead a collection of pre-build rules, one
-- for each component. The reason is that Backpack creates components on-the-fly
-- through instantiation, which means e.g. that a single component name can
-- resolve to multiple components. This means we really need to pass in the
-- components to the function, as we don't know the full details (e.g. their
-- unit ids) ahead of time.

instance Semigroup BuildHooks where
  BuildHooks
    { preBuildComponentRules = rs1
    , postBuildComponentHook = post1
    }
    <> BuildHooks
      { preBuildComponentRules = rs2
      , postBuildComponentHook = post2
      } =
      BuildHooks
        { preBuildComponentRules = rs1 <> rs2
        , postBuildComponentHook = post1 <> post2
        }

instance Monoid BuildHooks where
  mempty = noBuildHooks

-- | Empty build hooks.
noBuildHooks :: BuildHooks
noBuildHooks =
  BuildHooks
    { preBuildComponentRules = Nothing
    , postBuildComponentHook = Nothing
    }

--------------------------------------------------------------------------------
-- Install setup hooks.

data InstallComponentInputs = InstallComponentInputs
  { copyFlags :: CopyFlags
  , localBuildInfo :: LocalBuildInfo
  , targetInfo :: TargetInfo
  }
  deriving (Generic, Show)

-- | A per-component install hook,
-- which can only perform side effects (e.g. copying files).
type InstallComponentHook = InstallComponentInputs -> IO ()

-- | Copy/install hooks.
data InstallHooks = InstallHooks
  { installComponentHook :: Maybe InstallComponentHook
  -- ^ Per-component install hook.
  }

instance Semigroup InstallHooks where
  InstallHooks
    { installComponentHook = inst1
    }
    <> InstallHooks
      { installComponentHook = inst2
      } =
      InstallHooks
        { installComponentHook = inst1 <> inst2
        }

instance Monoid InstallHooks where
  mempty = noInstallHooks

-- | Empty copy/install hooks.
noInstallHooks :: InstallHooks
noInstallHooks =
  InstallHooks
    { installComponentHook = Nothing
    }

--------------------------------------------------------------------------------
-- Per-component configure hook implementation details.

type LibraryDiff = Library
type ForeignLibDiff = ForeignLib
type ExecutableDiff = Executable
type TestSuiteDiff = TestSuite
type BenchmarkDiff = Benchmark
type BuildInfoDiff = BuildInfo

-- | A diff to a Cabal 'Component', that gets combined monoidally into
-- an existing 'Component'.
newtype ComponentDiff = ComponentDiff {componentDiff :: Component}
  deriving (Semigroup, Show)

emptyComponentDiff :: ComponentName -> ComponentDiff
emptyComponentDiff name = ComponentDiff $
  case name of
    CLibName{} -> CLib emptyLibrary
    CFLibName{} -> CFLib emptyForeignLib
    CExeName{} -> CExe emptyExecutable
    CTestName{} -> CTest emptyTestSuite
    CBenchName{} -> CBench emptyBenchmark

buildInfoComponentDiff :: ComponentName -> BuildInfo -> ComponentDiff
buildInfoComponentDiff name bi = ComponentDiff $
  BI.buildInfo .~ bi $
    case name of
      CLibName{} -> CLib emptyLibrary
      CFLibName{} -> CFLib emptyForeignLib
      CExeName{} -> CExe emptyExecutable
      CTestName{} -> CTest emptyTestSuite
      CBenchName{} -> CBench emptyBenchmark

applyLibraryDiff :: Verbosity -> Library -> LibraryDiff -> IO Library
applyLibraryDiff verbosity lib diff =
  case illegalLibraryDiffReasons lib diff of
    [] -> return $ lib <> diff
    (r : rs) ->
      dieWithException verbosity $
        SetupHooksException $
          CannotApplyComponentDiff $
            IllegalComponentDiff (CLib lib) (r NE.:| rs)

illegalLibraryDiffReasons :: Library -> LibraryDiff -> [IllegalComponentDiffReason]
illegalLibraryDiffReasons
  lib
  Library
    { libName = nm
    , libExposed = e
    , libVisibility = vis
    , libBuildInfo = bi
    } =
    [ CannotChangeName
    | not $ nm == libName emptyLibrary || nm == libName lib
    ]
      ++ [ CannotChangeComponentField "libExposed"
         | not $ e == libExposed emptyLibrary || e == libExposed lib
         ]
      ++ [ CannotChangeComponentField "libVisibility"
         | not $ vis == libVisibility emptyLibrary || vis == libVisibility lib
         ]
      ++ illegalBuildInfoDiffReasons (libBuildInfo lib) bi

applyForeignLibDiff :: Verbosity -> ForeignLib -> ForeignLibDiff -> IO ForeignLib
applyForeignLibDiff verbosity flib diff =
  case illegalForeignLibDiffReasons flib diff of
    [] -> return $ flib <> diff
    (r : rs) ->
      dieWithException verbosity $
        SetupHooksException $
          CannotApplyComponentDiff $
            IllegalComponentDiff (CFLib flib) (r NE.:| rs)

illegalForeignLibDiffReasons :: ForeignLib -> ForeignLibDiff -> [IllegalComponentDiffReason]
illegalForeignLibDiffReasons
  flib
  ForeignLib
    { foreignLibName = nm
    , foreignLibType = ty
    , foreignLibOptions = opts
    , foreignLibVersionInfo = vi
    , foreignLibVersionLinux = linux
    , foreignLibModDefFile = defs
    , foreignLibBuildInfo = bi
    } =
    [ CannotChangeName
    | not $ nm == foreignLibName emptyForeignLib || nm == foreignLibName flib
    ]
      ++ [ CannotChangeComponentField "foreignLibType"
         | not $ ty == foreignLibType emptyForeignLib || ty == foreignLibType flib
         ]
      ++ [ CannotChangeComponentField "foreignLibOptions"
         | not $ opts == foreignLibOptions emptyForeignLib || opts == foreignLibOptions flib
         ]
      ++ [ CannotChangeComponentField "foreignLibVersionInfo"
         | not $ vi == foreignLibVersionInfo emptyForeignLib || vi == foreignLibVersionInfo flib
         ]
      ++ [ CannotChangeComponentField "foreignLibVersionLinux"
         | not $ linux == foreignLibVersionLinux emptyForeignLib || linux == foreignLibVersionLinux flib
         ]
      ++ [ CannotChangeComponentField "foreignLibModDefFile"
         | not $ defs == foreignLibModDefFile emptyForeignLib || defs == foreignLibModDefFile flib
         ]
      ++ illegalBuildInfoDiffReasons (foreignLibBuildInfo flib) bi

applyExecutableDiff :: Verbosity -> Executable -> ExecutableDiff -> IO Executable
applyExecutableDiff verbosity exe diff =
  case illegalExecutableDiffReasons exe diff of
    [] -> return $ exe <> diff
    (r : rs) ->
      dieWithException verbosity $
        SetupHooksException $
          CannotApplyComponentDiff $
            IllegalComponentDiff (CExe exe) (r NE.:| rs)

illegalExecutableDiffReasons :: Executable -> ExecutableDiff -> [IllegalComponentDiffReason]
illegalExecutableDiffReasons
  exe
  Executable
    { exeName = nm
    , modulePath = path
    , exeScope = scope
    , buildInfo = bi
    } =
    [ CannotChangeName
    | not $ nm == exeName emptyExecutable || nm == exeName exe
    ]
      ++ [ CannotChangeComponentField "modulePath"
         | not $ path == modulePath emptyExecutable || path == modulePath exe
         ]
      ++ [ CannotChangeComponentField "exeScope"
         | not $ scope == exeScope emptyExecutable || scope == exeScope exe
         ]
      ++ illegalBuildInfoDiffReasons (buildInfo exe) bi

applyTestSuiteDiff :: Verbosity -> TestSuite -> TestSuiteDiff -> IO TestSuite
applyTestSuiteDiff verbosity test diff =
  case illegalTestSuiteDiffReasons test diff of
    [] -> return $ test <> diff
    (r : rs) ->
      dieWithException verbosity $
        SetupHooksException $
          CannotApplyComponentDiff $
            IllegalComponentDiff (CTest test) (r NE.:| rs)

illegalTestSuiteDiffReasons :: TestSuite -> TestSuiteDiff -> [IllegalComponentDiffReason]
illegalTestSuiteDiffReasons
  test
  TestSuite
    { testName = nm
    , testInterface = iface
    , testCodeGenerators = gens
    , testBuildInfo = bi
    } =
    [ CannotChangeName
    | not $ nm == testName emptyTestSuite || nm == testName test
    ]
      ++ [ CannotChangeComponentField "testInterface"
         | not $ iface == testInterface emptyTestSuite || iface == testInterface test
         ]
      ++ [ CannotChangeComponentField "testCodeGenerators"
         | not $ gens == testCodeGenerators emptyTestSuite || gens == testCodeGenerators test
         ]
      ++ illegalBuildInfoDiffReasons (testBuildInfo test) bi

applyBenchmarkDiff :: Verbosity -> Benchmark -> BenchmarkDiff -> IO Benchmark
applyBenchmarkDiff verbosity bench diff =
  case illegalBenchmarkDiffReasons bench diff of
    [] -> return $ bench <> diff
    (r : rs) ->
      dieWithException verbosity $
        SetupHooksException $
          CannotApplyComponentDiff $
            IllegalComponentDiff (CBench bench) (r NE.:| rs)

illegalBenchmarkDiffReasons :: Benchmark -> BenchmarkDiff -> [IllegalComponentDiffReason]
illegalBenchmarkDiffReasons
  bench
  Benchmark
    { benchmarkName = nm
    , benchmarkInterface = iface
    , benchmarkBuildInfo = bi
    } =
    [ CannotChangeName
    | not $ nm == benchmarkName emptyBenchmark || nm == benchmarkName bench
    ]
      ++ [ CannotChangeComponentField "benchmarkInterface"
         | not $ iface == benchmarkInterface emptyBenchmark || iface == benchmarkInterface bench
         ]
      ++ illegalBuildInfoDiffReasons (benchmarkBuildInfo bench) bi

illegalBuildInfoDiffReasons :: BuildInfo -> BuildInfoDiff -> [IllegalComponentDiffReason]
illegalBuildInfoDiffReasons
  bi
  BuildInfo
    { buildable = can_build
    , buildTools = build_tools
    , buildToolDepends = build_tools_depends
    , pkgconfigDepends = pkgconfig_depends
    , frameworks = fworks
    , targetBuildDepends = target_build_depends
    } =
    map CannotChangeBuildInfoField $
      [ "buildable"
      | not $ can_build == buildable bi || can_build == buildable emptyBuildInfo
      ]
        ++ [ "buildTools"
           | not $ build_tools == buildTools bi || build_tools == buildTools emptyBuildInfo
           ]
        ++ [ "buildToolsDepends"
           | not $ build_tools_depends == buildToolDepends bi || build_tools_depends == buildToolDepends emptyBuildInfo
           ]
        ++ [ "pkgconfigDepends"
           | not $ pkgconfig_depends == pkgconfigDepends bi || pkgconfig_depends == pkgconfigDepends emptyBuildInfo
           ]
        ++ [ "frameworks"
           | not $ fworks == frameworks bi || fworks == frameworks emptyBuildInfo
           ]
        ++ [ "targetBuildDepends"
           | not $ target_build_depends == targetBuildDepends bi || target_build_depends == targetBuildDepends emptyBuildInfo
           ]

-- | Traverse the components of a 'PackageDescription'.
--
-- The function must preserve the component type, i.e. map a 'CLib' to a 'CLib',
-- a 'CExe' to a 'CExe', etc.
traverseComponents
  :: Applicative m
  => (Component -> m Component)
  -> PackageDescription
  -> m PackageDescription
traverseComponents f pd =
  upd_pd
    <$> traverse f_lib (library pd)
    <*> traverse f_lib (subLibraries pd)
    <*> traverse f_flib (foreignLibs pd)
    <*> traverse f_exe (executables pd)
    <*> traverse f_test (testSuites pd)
    <*> traverse f_bench (benchmarks pd)
  where
    f_lib lib = \case { CLib lib' -> lib'; c -> mismatch (CLib lib) c } <$> f (CLib lib)
    f_flib flib = \case { CFLib flib' -> flib'; c -> mismatch (CFLib flib) c } <$> f (CFLib flib)
    f_exe exe = \case { CExe exe' -> exe'; c -> mismatch (CExe exe) c } <$> f (CExe exe)
    f_test test = \case { CTest test' -> test'; c -> mismatch (CTest test) c } <$> f (CTest test)
    f_bench bench = \case { CBench bench' -> bench'; c -> mismatch (CBench bench) c } <$> f (CBench bench)

    upd_pd lib sublibs flibs exes tests benchs =
      pd
        { library = lib
        , subLibraries = sublibs
        , foreignLibs = flibs
        , executables = exes
        , testSuites = tests
        , benchmarks = benchs
        }

    -- This is a panic, because we maintain this invariant elsewhere:
    -- see 'componentDiffError' in 'applyComponentDiff', which catches an
    -- invalid per-component configure hook.
    mismatch c1 c2 =
      error $
        "Mismatched component types: "
          ++ showComponentName (componentName c1)
          ++ " "
          ++ showComponentName (componentName c2)
          ++ "."
{-# INLINEABLE traverseComponents #-}

applyComponentDiffs
  :: Verbosity
  -> (Component -> IO (Maybe ComponentDiff))
  -> PackageDescription
  -> IO PackageDescription
applyComponentDiffs verbosity f = traverseComponents apply_diff
  where
    apply_diff :: Component -> IO Component
    apply_diff c = do
      mbDiff <- f c
      case mbDiff of
        Just diff -> applyComponentDiff verbosity c diff
        Nothing -> return c

forComponents_ :: PackageDescription -> (Component -> IO ()) -> IO ()
forComponents_ pd f = getConst $ traverseComponents (Const . f) pd

applyComponentDiff
  :: Verbosity
  -> Component
  -> ComponentDiff
  -> IO Component
applyComponentDiff verbosity comp (ComponentDiff diff)
  | CLib lib <- comp
  , CLib lib_diff <- diff =
      CLib <$> applyLibraryDiff verbosity lib lib_diff
  | CFLib flib <- comp
  , CFLib flib_diff <- diff =
      CFLib <$> applyForeignLibDiff verbosity flib flib_diff
  | CExe exe <- comp
  , CExe exe_diff <- diff =
      CExe <$> applyExecutableDiff verbosity exe exe_diff
  | CTest test <- comp
  , CTest test_diff <- diff =
      CTest <$> applyTestSuiteDiff verbosity test test_diff
  | CBench bench <- comp
  , CBench bench_diff <- diff =
      CBench <$> applyBenchmarkDiff verbosity bench bench_diff
  | otherwise =
      componentDiffError $ MismatchedComponentTypes comp diff
  where
    -- The per-component configure hook specified a diff of the wrong type,
    -- e.g. tried to apply an executable diff to a library.
    componentDiffError err =
      dieWithException verbosity $
        SetupHooksException $
          CannotApplyComponentDiff err

--------------------------------------------------------------------------------
-- Running pre-build rules

-- | Run all pre-build rules.
--
-- This function should only be called internally within @Cabal@, as it is used
-- to implement the (legacy) Setup.hs interface. The build tool
-- (e.g. @cabal-install@ or @hls@) should instead go through the separate
-- hooks executable, which allows us to only rerun the out-of-date rules
-- (instead of running all of these rules at once).
executeRules
  :: Verbosity
  -> LocalBuildInfo
  -> TargetInfo
  -> Map RuleId Rule
  -> IO ()
executeRules =
  executeRulesUserOrSystem
    SUser
    (\_rId cmd -> sequenceA $ runRuleDynDepsCmd cmd)
    (\_rId cmd -> runRuleExecCmd cmd)

-- | Like 'executeRules', except it can be used when communicating with
-- an external hooks executable.
executeRulesUserOrSystem
  :: forall userOrSystem
   . SScope userOrSystem
  -> (RuleId -> RuleDynDepsCmd userOrSystem -> IO (Maybe ([Rule.Dependency], LBS.ByteString)))
  -> (RuleId -> RuleExecCmd userOrSystem -> IO ())
  -> Verbosity
  -> LocalBuildInfo
  -> TargetInfo
  -> Map RuleId (RuleData userOrSystem)
  -> IO ()
executeRulesUserOrSystem scope runDepsCmdData runCmdData verbosity lbi tgtInfo allRules = do
  -- Compute all extra dynamic dependency edges.
  dynDepsEdges <-
    flip Map.traverseMaybeWithKey allRules $
      \rId (Rule{ruleCommands = cmds}) ->
        runDepsCmdData rId (ruleDepsCmd cmds)

  -- Create a build graph of all the rules, with static and dynamic dependencies
  -- as edges.
  let
    (ruleGraph, ruleFromVertex, vertexFromRuleId) =
      Graph.graphFromEdges
        [ (rule, rId, nub $ mapMaybe directRuleDependencyMaybe allDeps)
        | (rId, rule) <- Map.toList allRules
        , let dynDeps = fromMaybe [] (fst <$> Map.lookup rId dynDepsEdges)
              allDeps = staticDependencies rule ++ dynDeps
        ]

    -- Topologically sort the graph of rules.
    sccs = Graph.scc ruleGraph
    cycles = mapMaybe $ \(Graph.Node v0 subforest) ->
      case subforest of
        []
          | r@(_, rId, deps) <- ruleFromVertex v0 ->
              if rId `elem` deps
                then Just (r, [])
                else Nothing
        v : vs ->
          Just
            ( ruleFromVertex v0
            , map (fmap ruleFromVertex) (v : vs)
            )

    -- Compute demanded rules.
    --
    -- SetupHooks TODO: maybe requiring all generated modules to appear
    -- in autogen-modules is excessive; we can look through all modules instead.
    autogenModPaths =
      map (\m -> moduleNameSymbolicPath m <.> "hs") $
        autogenModules $
          componentBuildInfo $
            targetComponent tgtInfo
    leafRule_maybe (rId, r) =
      if any ((r `ruleOutputsLocation`) . (Location compAutogenDir)) autogenModPaths
        then vertexFromRuleId rId
        else Nothing
    leafRules = mapMaybe leafRule_maybe $ Map.toList allRules
    demandedRuleVerts = Set.fromList $ concatMap (Graph.reachable ruleGraph) leafRules
    nonDemandedRuleVerts = Set.fromList (Graph.vertices ruleGraph) Set.\\ demandedRuleVerts

  case cycles sccs of
    -- If there are cycles in the dependency structure, don't execute
    -- any rules at all; just throw an error right off the bat.
    r : rs ->
      let getRule ((ru, _, _), js) = (toRuleBinary ru, fmap (fmap (\(rv, _, _) -> toRuleBinary rv)) js)
       in errorOut $
            CyclicRuleDependencies $
              fmap getRule (r NE.:| rs)
    -- Otherwise, run all the demanded rules in dependency order (in one go).
    -- (Fine-grained running of rules should happen in cabal-install or HLS,
    -- not in the Cabal library.)
    [] -> do
      -- Emit a warning if there are non-demanded rules.
      unless (null nonDemandedRuleVerts) $
        warn verbosity $
          unlines $
            "The following rules are not demanded and will not be run:"
              : concat
                [ [ "  - " ++ show rId ++ ","
                  , "    generating " ++ show (NE.toList $ results r)
                  ]
                | v <- Set.toList nonDemandedRuleVerts
                , let (r, rId, _) = ruleFromVertex v
                ]
              ++ [ "Possible reasons for this error:"
                 , "  - Some autogenerated modules were not declared"
                 , "    (in the package description or in the pre-configure hooks)"
                 , "  - The output location for an autogenerated module is incorrect,"
                 , "    (e.g. the file extension is incorrect, or"
                 , "     it is not in the appropriate 'autogenComponentModules' directory)"
                 ]

      -- Run all the demanded rules, in dependency order.
      for_ sccs $ \(Graph.Node ruleVertex _) ->
        -- Don't run a rule unless it is demanded.
        unless (ruleVertex `Set.member` nonDemandedRuleVerts) $ do
          let ( r@Rule
                  { ruleCommands = cmds
                  , staticDependencies = staticDeps
                  , results = reslts
                  }
                , rId
                , _staticRuleDepIds
                ) =
                  ruleFromVertex ruleVertex
              mbDyn = Map.lookup rId dynDepsEdges
              allDeps = staticDeps ++ fromMaybe [] (fst <$> mbDyn)
          -- Check that the dependencies the rule expects are indeed present.
          resolvedDeps <- traverse (resolveDependency verbosity rId allRules) allDeps
          missingRuleDeps <- filterM (missingDep mbWorkDir) resolvedDeps
          case NE.nonEmpty missingRuleDeps of
            Just missingDeps ->
              errorOut $ CantFindSourceForRuleDependencies (toRuleBinary r) missingDeps
            -- Dependencies OK: run the associated action.
            Nothing -> do
              let execCmd = ruleExecCmd scope cmds (snd <$> mbDyn)
              runCmdData rId execCmd
              -- Throw an error if running the action did not result in
              -- the generation of outputs that we expected it to.
              missingRuleResults <- filterM (missingDep mbWorkDir) $ NE.toList reslts
              for_ (NE.nonEmpty missingRuleResults) $ \missingResults ->
                errorOut $ MissingRuleOutputs (toRuleBinary r) missingResults
              return ()
  where
    toRuleBinary :: RuleData userOrSystem -> RuleBinary
    toRuleBinary = case scope of
      SUser -> ruleBinary
      SSystem -> id
    clbi = targetCLBI tgtInfo
    mbWorkDir = mbWorkDirLBI lbi
    compAutogenDir = autogenComponentModulesDir lbi clbi
    errorOut e =
      dieWithException verbosity $
        SetupHooksException $
          RulesException e

directRuleDependencyMaybe :: Rule.Dependency -> Maybe RuleId
directRuleDependencyMaybe (RuleDependency dep) = Just $ outputOfRule dep
directRuleDependencyMaybe (FileDependency{}) = Nothing

resolveDependency :: Verbosity -> RuleId -> Map RuleId (RuleData scope) -> Rule.Dependency -> IO Location
resolveDependency verbosity rId allRules = \case
  FileDependency l -> return l
  RuleDependency (RuleOutput{outputOfRule = depId, outputIndex = i}) ->
    case Map.lookup depId allRules of
      Nothing ->
        error $
          unlines $
            [ "Internal error: missing rule dependency."
            , "Rule: " ++ show rId
            , "Dependency: " ++ show depId
            ]
      Just (Rule{results = os}) ->
        let j :: Int
            j = fromIntegral i
         in case listToMaybe $ drop j $ NE.toList os of
              Just o
                | j >= 0 ->
                    return o
              _ ->
                dieWithException verbosity $
                  SetupHooksException $
                    RulesException $
                      InvalidRuleOutputIndex rId depId os i

-- | Does the rule output the given location?
ruleOutputsLocation :: RuleData scope -> Location -> Bool
ruleOutputsLocation (Rule{results = rs}) fp =
  any (\out -> normaliseLocation out == normaliseLocation fp) rs

normaliseLocation :: Location -> Location
normaliseLocation (Location base rel) =
  Location (normaliseSymbolicPath base) (normaliseSymbolicPath rel)

-- | Is the file we depend on missing?
missingDep :: Maybe (SymbolicPath CWD (Dir Pkg)) -> Location -> IO Bool
missingDep mbWorkDir loc = not <$> doesFileExist fp
  where
    fp = interpretSymbolicPath mbWorkDir (location loc)

--------------------------------------------------------------------------------
-- Compatibility with HookedBuildInfo.
--
-- NB: assumes that the components in HookedBuildInfo are:
--  - an (optional) main library,
--  - executables.
--
-- No support for named sublibraries, foreign libraries, tests or benchmarks,
-- because the HookedBuildInfo datatype doesn't specify what type of component
-- each component name is (so we assume they are executables).

hookedBuildInfoComponents :: HookedBuildInfo -> Set ComponentName
hookedBuildInfoComponents (mb_mainlib, exes) =
  Set.fromList $
    (case mb_mainlib of Nothing -> id; Just{} -> (CLibName LMainLibName :))
      [CExeName exe_nm | (exe_nm, _) <- exes]

hookedBuildInfoComponentDiff_maybe :: HookedBuildInfo -> ComponentName -> Maybe (IO ComponentDiff)
hookedBuildInfoComponentDiff_maybe (mb_mainlib, exes) comp_nm =
  case comp_nm of
    CLibName lib_nm ->
      case lib_nm of
        LMainLibName -> return . ComponentDiff . CLib . buildInfoLibraryDiff <$> mb_mainlib
        LSubLibName{} -> Nothing
    CExeName exe_nm ->
      let mb_exe = lookup exe_nm exes
       in return . ComponentDiff . CExe . buildInfoExecutableDiff <$> mb_exe
    CFLibName{} -> Nothing
    CTestName{} -> Nothing
    CBenchName{} -> Nothing

buildInfoLibraryDiff :: BuildInfo -> LibraryDiff
buildInfoLibraryDiff bi = emptyLibrary{libBuildInfo = bi}

buildInfoExecutableDiff :: BuildInfo -> ExecutableDiff
buildInfoExecutableDiff bi = emptyExecutable{buildInfo = bi}

--------------------------------------------------------------------------------
-- Instances for serialisation

deriving newtype instance Binary ComponentDiff
deriving newtype instance Structured ComponentDiff

instance Binary PreConfPackageInputs
instance Structured PreConfPackageInputs
instance Binary PreConfPackageOutputs
instance Structured PreConfPackageOutputs

instance Binary PostConfPackageInputs
instance Structured PostConfPackageInputs

instance Binary PreConfComponentInputs
instance Structured PreConfComponentInputs
instance Binary PreConfComponentOutputs
instance Structured PreConfComponentOutputs

instance Binary PreBuildComponentInputs
instance Structured PreBuildComponentInputs

instance Binary PostBuildComponentInputs
instance Structured PostBuildComponentInputs

instance Binary InstallComponentInputs
instance Structured InstallComponentInputs

--------------------------------------------------------------------------------
