packages feed

hooks-exe-3.18: cli/Distribution/Client/SetupHooks/CallHooksExe.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- HLINT ignore "Use curry" -}

module Distribution.Client.SetupHooks.CallHooksExe
  ( callHooksExe
  , externalSetupHooks
  , externalSetupHooksABI
  , buildTypeSetupHooks
  , buildTypePreBuildHooks
  , runExternalPreBuildRules
  , hooksProgFilePath
  ) where

-- base
import GHC.Stack

-- bytestring
import Data.ByteString.Lazy as LBS
  ( hGetContents
  , hPut
  , null
  )

-- process
import qualified System.Process as P
import System.Process.CommunicationHandle
  ( readCreateProcessWithExitCodeCommunicationHandle )

-- filepath
import System.FilePath
  ( (</>), (<.>) )

-- Cabal
import Distribution.Compat.Prelude
import qualified Distribution.Compat.Binary as Binary
import Distribution.Simple
  ( autoconfSetupHooks )
import Distribution.Simple.Build
  ( builtinPreBuildHooks )
import Distribution.Simple.BuildPaths
  ( exeExtension )
import Distribution.Simple.SetupHooks.Internal
import Distribution.Simple.SetupHooks.Rule
import Distribution.Simple.Utils
  ( dieWithException )
import Distribution.System
  ( buildPlatform )
import Distribution.Types.BuildType
  ( BuildType(..) )
import Distribution.Utils.Path
  ( CWD
  , Dist
  , Pkg
  , SymbolicPath
  , FileOrDir(..)
  , interpretSymbolicPath
  )
import Distribution.Verbosity
  ( Verbosity, VerbosityHandles, mkVerbosity )

-- hooks-cli
import Distribution.Client.SetupHooks.CallHooksExe.Errors
import Distribution.Simple.SetupHooks.HooksMain
  ( HooksVersion )

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

type HookIO inputs outputs =
  ( HasCallStack
  , Typeable inputs, Typeable outputs
  , Binary inputs, Binary outputs
  )

-- | Call an external hooks executable in order to execute a Cabal Setup hook.
callHooksExe
  :: forall inputs outputs
  .  HookIO inputs outputs
  => Verbosity
  -> FilePath -- ^ path to hooks executable
  -> String   -- ^ name of the hook to run
  -> inputs   -- ^ argument to the hook
  -> IO outputs
callHooksExe verb hooksExe hookName input = do
  (ex, output) <-
    -- The arguments to the external hooks executable are:
    --
    --  1. Input handle, from which the hooks executable receives its input.
    --  2. Output handle, to which the hooks executable writes its output.
    --  3. The hook type to run.
    --
    -- The hooks executable will read input from the input handle, decode it,
    -- run the necessary hook, producing a result which it encodes and writes
    -- to the output handle.
    readCreateProcessWithExitCodeCommunicationHandle
      ( \(theyRead, theyWrite) -> P.proc hooksExe [show theyRead, show theyWrite, hookName] )
      ( \ hWeRead -> hGetContents hWeRead )
      ( \ hWeWrite -> do
        let i = Binary.encode input
        unless (LBS.null i) $
          hPut hWeWrite i
      )
  case ex of
    ExitFailure exitCode ->
      dieWithException verb $
        HookFailed hookName $
          HookException exitCode
    ExitSuccess -> do
      let mbOutput = Binary.decodeOrFail output
      case mbOutput of
        Left (_, offset, err) -> do
          dieWithException verb $
            HookFailed hookName $
              CouldNotDecodeOutput output offset err
        Right (_, _, res) -> return res

-- | Construct a 'SetupHooks' that runs the hooks of the external hooks executable
-- at the given path through the CLI.
--
-- This should only be used at the final step of compiling a package, when we
-- have all the hooks in hand. The SetupHooks that are returned by this function
-- cannot be combined with any other SetupHooks; they must directly be used to
-- build the package.
externalSetupHooks :: Verbosity -> FilePath -> SetupHooks
externalSetupHooks verb hooksExe =
  SetupHooks
    { configureHooks =
        ConfigureHooks
          { preConfPackageHook = Just $ hook "preConfPackage"
          , postConfPackageHook = Just $ hook "postConfPackage"
          , preConfComponentHook = Just $ hook "preConfComponent"
          }
    , buildHooks =
        BuildHooks
          { -- NB: external pre-build rules are special, due to the StaticPtr machinery.
            -- To invoke them, we must separately call 'runExternalPreBuildRules'.
            preBuildComponentRules = Nothing
          , postBuildComponentHook = Just $ hook "postBuildComponent"
          }
    , installHooks =
        InstallHooks
          { installComponentHook = Just $ hook "installComponent"
          }
    }
  where
    hook :: HookIO inputs outputs => String -> inputs -> IO outputs
    hook = callHooksExe verb hooksExe

-- | The ABI of an external hooks executable.
--
-- This information is used to handshake before further communication,
-- in order to avoid a cascade of errors with mismatched 'Binary' instances.
externalSetupHooksABI :: Verbosity -> FilePath -> IO HooksVersion
externalSetupHooksABI verb hooksExe =
  callHooksExe verb hooksExe "version" ()

-- | The 'SetupHooks' associated to a particular 'BuildType'.
--
-- **Warning:** for @build-type: Hooks@, this does not include the pre-build
-- hooks. Those can be retrieved with 'buildTypePreBuildHooks'.
buildTypeSetupHooks
  :: Verbosity
  -> Maybe (SymbolicPath CWD (Dir Pkg))
  -> SymbolicPath Pkg (Dir Dist)
  -> BuildType
  -> SetupHooks
buildTypeSetupHooks verb mbWorkDir distPref = \case
  Hooks -> externalSetupHooks verb $ hooksProgFilePath mbWorkDir distPref
  Configure -> autoconfSetupHooks
  _ -> noSetupHooks
    -- Note: if any built-in functionality is implemented using SetupHooks,
    -- we would also need to include those.

-- | The pre-build hooks obtained by communication with an external hooks executable.
buildTypePreBuildHooks
  :: VerbosityHandles
  -> Maybe (SymbolicPath CWD (Dir Pkg))
  -> SymbolicPath Pkg (Dir Dist)
  -> BuildType
  -> ( PreBuildComponentInputs -> IO [MonitorFilePath] )
buildTypePreBuildHooks verbHandles mbWorkDir distPref bt pbci = do
  builtinMons <- builtinPreBuildHooks bt pbci
  externalMons <- case bt of
    Hooks ->
      runExternalPreBuildRules verbHandles
        (hooksProgFilePath mbWorkDir distPref)
        pbci
    _ -> return []
  return (builtinMons ++ externalMons)

-- | Run pre-build rules coming from an external hooks executable at the
-- given filepath.
--
-- Note that 'executeRulesUserOrSystem' handles recompilation checking, only
-- re-running rules that are stale.
runExternalPreBuildRules
  :: VerbosityHandles
  -> FilePath -- ^ path to external hooks executable
  -> PreBuildComponentInputs
  -> IO [MonitorFilePath]
runExternalPreBuildRules verbHandles hooksExe
  pbci@PreBuildComponentInputs
    { buildingWhat = what
    , localBuildInfo = lbi
    , targetInfo = tgt } = do
  let verbFlags = buildingWhatVerbosity what
      verbosity = mkVerbosity verbHandles verbFlags
      hook :: HookIO inputs outputs => String -> inputs -> IO outputs
      hook = callHooksExe verbosity hooksExe
  -- Here we make sure to use 'RuleBinary' (@'Scope' == 'System'@)
  -- to avoid looking up static pointer keys from the hooks executable
  -- from the outside (e.g. from within cabal-install).
  (rulesMap :: Map RuleId RuleBinary, monitors) <- hook "preBuildRules" pbci
  executeRulesUserOrSystem
    SSystem
    ( \ rId cmd -> case cmd of
      StaticRuleCommand {} -> return Nothing
      DynamicRuleCommands {} -> Just <$> hook "runPreBuildRuleDeps" (rId, cmd)
    )
    ( \ rId cmd -> hook "runPreBuildRule" (rId, cmd) )
    verbosity lbi tgt rulesMap
  return monitors

-- | The path to the external hooks executable.
hooksProgFilePath
  :: Maybe (SymbolicPath CWD (Dir Pkg))
  -> SymbolicPath Pkg (Dir Dist)
  -> FilePath
hooksProgFilePath mbWorkDir distPref =
  interpretSymbolicPath mbWorkDir distPref
    </> "setup"
    </> "hooks"
    <.> exeExtension buildPlatform