packages feed

Cabal-3.18.1.0: src/Distribution/Simple/SetupHooks/HooksMain.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}

-- | Implementation of hooks executables for @build-type: Hooks@ packages.
--
-- A hooks executable is a small program compiled from the @SetupHooks.hs@
-- module of a package with @build-type: Hooks@. Its @main@ function is:
--
-- > import Distribution.Simple.SetupHooks.HooksMain (hooksMain)
-- > import SetupHooks (setupHooks)
-- > main = hooksMain setupHooks
--
-- @cabal-install@ communicates with the external hooks executable to implement
-- the hooks in a package with @build-type: Hooks@.
module Distribution.Simple.SetupHooks.HooksMain
  ( -- * Main entry point for hooks executables
    hooksMain

    -- * Hooks version handshake
  , HooksVersion (..)
  , hooksVersion
  , CabalABI (..)
  , HooksABI (..)
  ) where

-- base
import Control.Monad
  ( (>=>)
  )
import Control.Monad.IO.Class
  ( liftIO
  )
import GHC.Exception
import System.Environment
  ( getArgs
  )
import System.IO
  ( Handle
  , hClose
  , hFlush
  )

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

-- containers
import qualified Data.Map as Map

-- process
import System.Process.CommunicationHandle
  ( openCommunicationHandleRead
  , openCommunicationHandleWrite
  )

-- transformers
import Control.Monad.Trans.Except
  ( ExceptT
  , runExceptT
  , throwE
  )

-- Cabal-syntax
import qualified Distribution.Compat.Binary as Binary
  ( decodeOrFail
  , encode
  )
import Distribution.Types.Version
  ( Version
  )
import Distribution.Utils.Structured
  ( MD5
  , structureHash
  )

-- Cabal
import Distribution.Compat.Prelude
import Distribution.Simple.SetupHooks.Internal
import Distribution.Simple.SetupHooks.Rule
import Distribution.Simple.Utils
  ( VerboseException (..)
  , cabalVersion
  , dieWithException
  , exceptionWithMetadata
  , withOutputMarker
  )
import Distribution.Types.Component
  ( componentName
  )
import qualified Distribution.Types.LocalBuildConfig as LBC
import Distribution.Types.LocalBuildInfo
  ( LocalBuildInfo
  )
import Distribution.Verbosity
  ( Verbosity
  , defaultVerbosityHandles
  , mkVerbosity
  )
import qualified Distribution.Verbosity as Verbosity
  ( normal
  )

--------------------------------------------------------------------------------
-- Hooks version

-- | The version of the Hooks API in use.
--
-- Used for handshake before beginning inter-process communication.
data HooksVersion = HooksVersion
  { hooksAPIVersion :: !Version
  , cabalABIHash :: !MD5
  , hooksABIHash :: !MD5
  }
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (Binary)

-- | The version of the Hooks API built into this version of the Cabal library.
--
-- Used for handshake before beginning inter-process communication.
hooksVersion :: HooksVersion
hooksVersion =
  HooksVersion
    { hooksAPIVersion = cabalVersion
    , cabalABIHash = structureHash $ Proxy @CabalABI
    , hooksABIHash = structureHash $ Proxy @HooksABI
    }

-- | Tracks the parts of the Cabal API relevant to its binary interface.
data CabalABI = CabalABI
  { cabalLocalBuildInfo :: LocalBuildInfo
  }
  deriving stock (Generic)

deriving anyclass instance Structured CabalABI

-- | Tracks the parts of the Hooks API relevant to its binary interface.
data HooksABI = HooksABI
  { confHooks
      :: ( (PreConfPackageInputs, PreConfPackageOutputs)
         , PostConfPackageInputs
         , (PreConfComponentInputs, PreConfComponentOutputs)
         )
  , buildHooks
      :: ( PreBuildComponentInputs
         , (RuleId, Rule, RuleBinary)
         , PostBuildComponentInputs
         )
  , installHooks :: InstallComponentInputs
  }
  deriving stock (Generic)

deriving anyclass instance Structured HooksABI

--------------------------------------------------------------------------------
-- Error types (internal)

data SetupHooksExeException
  = -- | Missing hook type argument.
    NoHookType
  | -- | Could not parse a communication handle argument.
    NoHandle (Maybe String)
  | -- | Incorrect arguments passed to the hooks executable.
    BadHooksExeArgs
      String
      -- ^ hook name
      BadHooksExecutableArgs
  deriving (Show)

-- | An error describing an invalid argument passed to a hooks executable.
data BadHooksExecutableArgs
  = -- | Unknown hook type was requested.
    UnknownHookType
      {knownHookTypes :: [String]}
  | -- | Failed to decode the binary input to a hook.
    CouldNotDecodeInput
      ByteString
      -- ^ hook input that failed to decode
      Int64
      -- ^ byte offset at which decoding failed
      String
      -- ^ decoding error message
  | -- | The rule does not have a dynamic dependency computation.
    NoDynDepsCmd RuleId
  deriving (Show)

setupHooksExeExceptionCode :: SetupHooksExeException -> Int
setupHooksExeExceptionCode = \case
  NoHookType -> 7982
  NoHandle{} -> 8811
  BadHooksExeArgs _ rea -> badHooksExeArgsCode rea

setupHooksExeExceptionMessage :: SetupHooksExeException -> String
setupHooksExeExceptionMessage = \case
  NoHookType ->
    "Missing argument to Hooks executable.\n\
    \Expected two arguments: communication handle and hook type."
  NoHandle Nothing ->
    "Missing argument to Hooks executable.\n\
    \Expected two arguments: communication handle and hook type."
  NoHandle (Just h) ->
    "Invalid handle reference passed to Hooks executable: '" ++ h ++ "'."
  BadHooksExeArgs hookName reason ->
    badHooksExeArgsMessage hookName reason

badHooksExeArgsCode :: BadHooksExecutableArgs -> Int
badHooksExeArgsCode = \case
  UnknownHookType{} -> 4229
  CouldNotDecodeInput{} -> 9121
  NoDynDepsCmd{} -> 3231

badHooksExeArgsMessage :: String -> BadHooksExecutableArgs -> String
badHooksExeArgsMessage hookName = \case
  UnknownHookType knownHookNames ->
    "Unknown hook type "
      ++ hookName
      ++ ".\n\
         \Known hook types are: "
      ++ show knownHookNames
      ++ "."
  CouldNotDecodeInput _bytes offset err ->
    "Failed to decode the input to the "
      ++ hookName
      ++ " hook.\n\
         \Decoding failed at position "
      ++ show offset
      ++ " with error: "
      ++ err
      ++ ".\n\
         \This could be due to a mismatch between the Cabal version of cabal-install\
         \ and of the hooks executable."
  NoDynDepsCmd rId ->
    unlines
      [ "Unexpected rule " <> show rId <> " in the " <> hookName <> " hook."
      , "The rule does not have an associated dynamic dependency computation."
      ]

instance Exception (VerboseException SetupHooksExeException) where
  displayException :: VerboseException SetupHooksExeException -> String
  displayException (VerboseException stack timestamp verb err) =
    withOutputMarker
      verb
      ( concat
          [ "Error: [Cabal-"
          , show (setupHooksExeExceptionCode err)
          , "]\n"
          ]
      )
      ++ exceptionWithMetadata stack timestamp verb (setupHooksExeExceptionMessage err)

-- | The verbosity used inside the hooks executable.
--
-- The hooks executable is always invoked as a separate process, so stdout
-- and stderr are available for verbosity output and can be redirected via
-- the @System.Process@ API.
hooksExeVerbosity :: Verbosity
hooksExeVerbosity = mkVerbosity defaultVerbosityHandles Verbosity.normal

--------------------------------------------------------------------------------
-- Main entry point

-- | Create a hooks executable @main@ given the package's 'SetupHooks'.
--
-- The executable expects three command-line arguments:
--
--  1. A reference to an input communication handle (to read hook inputs from).
--  2. A reference to an output communication handle (to write hook outputs to).
--  3. The hook type to run.
--
-- The hook reads binary-encoded data from the input handle, runs the
-- requested hook, and writes the binary-encoded result to the output handle.
hooksMain :: SetupHooks -> IO ()
hooksMain setupHooks = runHooksM $ do
  ((hRead, hWrite), hookName) <- getHooksMainArgs
  case lookup hookName allHookHandlers of
    Just handleAction ->
      handleAction (hRead, hWrite) setupHooks
    Nothing ->
      throwE $
        BadHooksExeArgs hookName $
          UnknownHookType
            { knownHookTypes = map fst allHookHandlers
            }
  where
    allHookHandlers = [(hookName h, hookHandler h) | h <- hookHandlers]

    -- Get the communication handles and the name of the hook to run
    getHooksMainArgs :: HooksM ((Handle, Handle), String)
    getHooksMainArgs =
      liftIO getArgs >>= \case
        inputFdRef : outputFdRef : hookNm : _ ->
          case (readMaybe inputFdRef, readMaybe outputFdRef) of
            (Just readNm, Just writeNm) -> do
              hRead <- liftIO $ openCommunicationHandleRead readNm
              hWrite <- liftIO $ openCommunicationHandleWrite writeNm
              return ((hRead, hWrite), hookNm)
            (Nothing, _) ->
              throwE $ NoHandle (Just $ "hook input communication handle '" ++ inputFdRef ++ "'")
            (_, Nothing) ->
              throwE $ NoHandle (Just $ "hook output communication handle '" ++ outputFdRef ++ "'")
        _ -> throwE $ NoHandle Nothing

type HooksM = ExceptT SetupHooksExeException IO

runHooksM :: HooksM a -> IO a
runHooksM = runExceptT >=> either (dieWithException hooksExeVerbosity) pure

-- | Run a hook by reading its input from a handle, invoking it, and writing
-- its output to another handle.
runHookHandle
  :: forall inputs outputs
   . (Binary inputs, Binary outputs)
  => (Handle, Handle)
  -- ^ Input and output communication handles
  -> String
  -- ^ Hook name (used in error messages)
  -> (inputs -> HooksM outputs)
  -- ^ The hook to run
  -> HooksM ()
runHookHandle (hRead, hWrite) hookName hook = do
  inputsData <- liftIO $ LBS.hGetContents hRead
  let mb_inputs = Binary.decodeOrFail inputsData
  case mb_inputs of
    Left (_, offset, err) ->
      throwE $
        BadHooksExeArgs hookName $
          CouldNotDecodeInput inputsData offset err
    Right (_, _, inputs) ->
      hook inputs >>= \output -> liftIO $ do
        let outputData = Binary.encode output
        unless (LBS.null outputData) $
          LBS.hPutStr hWrite outputData
        hFlush hWrite
        hClose hWrite

data HookHandler = HookHandler
  { hookName :: !String
  , hookHandler :: (Handle, Handle) -> SetupHooks -> HooksM ()
  }

hookHandlers :: [HookHandler]
hookHandlers =
  [ let hookName = "version"
     in HookHandler hookName $ \h _ ->
          runHookHandle h hookName $ \() ->
            return hooksVersion
  , let hookName = "preConfPackage"
        noHook (PreConfPackageInputs{localBuildConfig = lbc}) =
          return $
            PreConfPackageOutputs
              { buildOptions = LBC.withBuildOptions lbc
              , extraConfiguredProgs = Map.empty
              }
     in HookHandler hookName $ \h (SetupHooks{configureHooks = ConfigureHooks{..}}) ->
          runHookHandle h hookName $ maybe noHook (liftIO .) preConfPackageHook
  , let hookName = "postConfPackage"
        noHook _ = return ()
     in HookHandler hookName $ \h (SetupHooks{configureHooks = ConfigureHooks{..}}) ->
          runHookHandle h hookName $ maybe noHook (liftIO .) postConfPackageHook
  , let hookName = "preConfComponent"
        noHook (PreConfComponentInputs{component = c}) =
          return $ PreConfComponentOutputs{componentDiff = emptyComponentDiff $ componentName c}
     in HookHandler hookName $ \h (SetupHooks{configureHooks = ConfigureHooks{..}}) ->
          runHookHandle h hookName $ maybe noHook (liftIO .) preConfComponentHook
  , let hookName = "preBuildRules"
     in HookHandler hookName $ \h (SetupHooks{buildHooks = BuildHooks{..}}) ->
          runHookHandle h hookName $ \preBuildInputs ->
            case preBuildComponentRules of
              Nothing -> return (Map.empty, [])
              Just pbcRules ->
                liftIO $
                  computeRules hooksExeVerbosity preBuildInputs pbcRules
  , let hookName = "runPreBuildRuleDeps"
     in HookHandler hookName $ \h _ ->
          runHookHandle h hookName $ \(ruleId, ruleDeps) ->
            case runRuleDynDepsCmd ruleDeps of
              Nothing ->
                throwE $
                  BadHooksExeArgs hookName $
                    NoDynDepsCmd ruleId
              Just getDeps -> liftIO getDeps
  , let hookName = "runPreBuildRule"
     in HookHandler hookName $ \h _ ->
          runHookHandle h hookName $ \(_ruleId :: RuleId, rExecCmd) ->
            liftIO $ runRuleExecCmd rExecCmd
  , let hookName = "postBuildComponent"
        noHook _ = return ()
     in HookHandler hookName $ \h (SetupHooks{buildHooks = BuildHooks{..}}) ->
          runHookHandle h hookName $ maybe noHook (liftIO .) postBuildComponentHook
  , let hookName = "installComponent"
        noHook _ = return ()
     in HookHandler hookName $ \h (SetupHooks{installHooks = InstallHooks{..}}) ->
          runHookHandle h hookName $ maybe noHook (liftIO .) installComponentHook
  ]