packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Boot.hs

module HsBindgen.Boot (
    runBoot
  , ClangArtefacts (..)
  , getClangArtefacts
  , BootArtefact (..)
  , BootMsg (..)
  ) where

import Control.Exception (displayException)
import Text.SimplePrettyPrint (CtxDoc, (><))
import Text.SimplePrettyPrint qualified as PP

import Clang.Args
import Clang.CStandard

import HsBindgen.Backend.Category (Category (..))
import HsBindgen.BindingSpec
import HsBindgen.Cache
import HsBindgen.Clang
import HsBindgen.Clang.CompareVersions (CompareVersionsMsg,
                                        compareClangVersions)
import HsBindgen.Clang.Discover
import HsBindgen.Clang.ExtraClangArgs
import HsBindgen.Clang.Macos
import HsBindgen.Config.ClangArgs (ClangArgsConfig)
import HsBindgen.Config.ClangArgs qualified as ClangArgs
import HsBindgen.Config.Internal
import HsBindgen.Imports
import HsBindgen.IR.C qualified as C
import HsBindgen.Macro.Interface qualified as Macro
import HsBindgen.Util.Tracer

-- | Boot phase.
--
-- Basic setup and checks.
--
-- - Check arguments to @#include@.
-- - Determine Clang arguments.
-- - Load external and prescriptive binding specifications.
runBoot ::
     Tracer BootMsg
  -> (ClangCStandard -> IO (Macro.Lang l))
  -> BindgenConfig
  -> [C.UncheckedRootDirective]
  -> IO (BootArtefact l)
runBoot tracer mkMacroLang config uncheckedRootDirectives = do
    traceStatus $ BootStatusStart config

    checkBackendConfig (contramap BootBackendConfig tracer) config.backend

    checkMacosEnv (contramap BootMacos tracer)

    getRootDirectives <- cache "rootDirectives" $ Cached $ do
      let tracer' = contramap BootHashIncludeArg tracer
      withTrace BootStatusRootDirectives $
        traverse (traverse (C.hashIncludeArgWithTrace tracer'))
          uncheckedRootDirectives

    getClangArtefacts' <- cache "clangArtefacts" $ Cached $
      getClangArtefacts tracer config.boot.clangArgs

    getCStandard <- cache "cStandard" $ withTrace BootStatusCStandard $
      (.cStandard) <$> getClangArtefacts'

    getClangExe <- cache "clangExe" $ withTrace BootStatusClangExe $
      (.clangExe) <$> getClangArtefacts'

    getClangArgs <- cache "clangArgs" $ withTrace BootStatusClangArgs $
      (.clangArgs) <$> getClangArtefacts'

    getMacroLang <- cache "macroLang" $ do
       cStandard <- getCStandard
       liftIO $ mkMacroLang cStandard

    getBindingSpecs <- cache "loadBindingSpecs" $ do
      clangArgs <- getClangArgs
      liftIO $ loadBindingSpecs
        (contramap BootBindingSpec tracer)
        clangArgs
        (fromBaseModuleName config.boot.baseModule (Just CType))
        config.boot.bindingSpec

    getExternalBindingSpecs <- cache "getExternalBindingSpecs" $
      withTrace BootStatusExternalBindingSpecs $
        fmap fst $ getBindingSpecs

    getPrescriptiveBindingSpec <- cache "getPrescriptiveBindingSpec" $
      withTrace BootStatusPrescriptiveBindingSpec $
        fmap snd $ getBindingSpecs

    pure BootArtefact {
          baseModule              = config.boot.baseModule
        , cStandard               = getCStandard
        , clangExe                = getClangExe
        , clangArgs               = getClangArgs
        , macroLang               = getMacroLang
        , rootDirectives          = getRootDirectives
        , externalBindingSpecs    = getExternalBindingSpecs
        , prescriptiveBindingSpec = getPrescriptiveBindingSpec
        }
  where
    tracerBootStatus :: Tracer BootStatusMsg
    tracerBootStatus = contramap BootStatus tracer

    traceStatus :: MonadIO m => BootStatusMsg -> m ()
    traceStatus = traceWith tracerBootStatus . withCallStack

    withTrace :: MonadIO m => (a -> BootStatusMsg) -> m a -> m a
    withTrace c a = do
      x <- a
      traceStatus $ c x
      pure x

    cache :: String -> Cached a -> IO (Cached a)
    cache = cacheWith (contramap (BootCache . SafeTrace) tracer) . Just

data ClangArtefacts = ClangArtefacts {
    cStandard :: ClangCStandard
  , clangExe  :: Maybe ClangExe
  , clangArgs :: ClangArgs
  }

-- | Determine Clang artefacts
getClangArtefacts ::
     Tracer BootMsg
  -> ClangArgsConfig FilePath
  -> IO ClangArtefacts
getClangArtefacts tracer config0 = do
    compareClangVersions (contramap BootCompareClangVersions tracer)
    extraClangArgs <- getExtraClangArgs tracerExtraClangArgs
    paths <- getPaths tracerDiscover config0.builtinIncDir
    let config =
            applyExtraClangArgs extraClangArgs
          . applyPaths paths
          $ config0
        clangArgs' = ClangArgs.clangArgsConfigToClangArgs config
    cStandard' <- getClangCStandard' tracer clangArgs'
    return ClangArtefacts{
        cStandard = cStandard'
      , clangExe  = paths.pClangExe
      , clangArgs = clangArgs'
      }
  where
    tracerDiscover :: Tracer DiscoverMsg
    tracerDiscover = contramap BootDiscover tracer

    tracerExtraClangArgs :: Tracer ExtraClangArgsMsg
    tracerExtraClangArgs = contramap BootExtraClangArgs tracer

-- | Fatal exceptions
data FatalException =
    UnableToDetermineCStandardException
  deriving Show

instance Exception FatalException where
  displayException = \case
    UnableToDetermineCStandardException -> "Unable to determine C standard"

-- | Determine the C standard using @libclang@
--
-- This function throws a 'UnableToDetermineCStandardException' if the call to
-- @libclang@ fails or we are unable to determine the C standard.
getClangCStandard' :: Tracer BootMsg -> ClangArgs -> IO ClangCStandard
getClangCStandard' tracer clangArgs =
    getClangCStandard clangArgs >>= \case
      Just clangCStandard -> do
        traceWith tracerCStandard $ withCallStack $ BootCStandardClang clangCStandard
        return clangCStandard
      Nothing -> do
        traceWith tracerCStandard $ withCallStack $ BootCStandardFail
        throwIO UnableToDetermineCStandardException
  where
    tracerCStandard :: Tracer BootCStandardMsg
    tracerCStandard = contramap BootCStandard tracer

{-------------------------------------------------------------------------------
  Artefact
-------------------------------------------------------------------------------}

data BootArtefact l = BootArtefact {
      baseModule              :: BaseModuleName
    , cStandard               :: Cached ClangCStandard
    , clangExe                :: Cached (Maybe ClangExe)
    , clangArgs               :: Cached ClangArgs
    , macroLang               :: Cached (Macro.Lang l)
    , rootDirectives          :: Cached [C.RootDirective C.HashIncludeArg]
    , externalBindingSpecs    :: Cached MergedBindingSpecs
    , prescriptiveBindingSpec :: Cached PrescriptiveBindingSpec
    }

{-------------------------------------------------------------------------------
  Trace
-------------------------------------------------------------------------------}

data BootStatusMsg =
    BootStatusStart                   BindgenConfig
  | BootStatusCStandard               ClangCStandard
  | BootStatusClangExe                (Maybe ClangExe)
  | BootStatusClangArgs               ClangArgs
  | BootStatusRootDirectives          [C.RootDirective C.HashIncludeArg]
  | BootStatusExternalBindingSpecs    MergedBindingSpecs
  | BootStatusPrescriptiveBindingSpec PrescriptiveBindingSpec
  deriving stock (Show, Generic)

bootStatus :: Show a => String -> a -> CtxDoc
bootStatus nm x =
  PP.hang ("Boot status (" >< PP.string nm >< "):") 2 $ PP.show x

instance PrettyForTrace BootStatusMsg where
  prettyForTrace = \case
    BootStatusStart                   x -> bootStatus "BindgenConfig"           x
    BootStatusCStandard               x -> bootStatus "ClangCStandard"          x
    BootStatusClangExe                x -> bootStatus "ClangExe"                x
    BootStatusClangArgs               x -> bootStatus "ClangArgs"               x
    BootStatusRootDirectives          x -> bootStatus "RootDirectives"          x
    BootStatusExternalBindingSpecs    x -> bootStatus "ExternalBindingSpecs"    x
    BootStatusPrescriptiveBindingSpec x -> bootStatus "PrescriptiveBindingSpec" x

instance IsTrace Level BootStatusMsg where
  getDefaultLogLevel = const Debug
  getSource          = const HsBindgen
  getTraceId         = const "boot-status"

data BootCStandardMsg =
    BootCStandardClang ClangCStandard
  | BootCStandardFail
  deriving stock (Show, Generic)

instance PrettyForTrace BootCStandardMsg where
  prettyForTrace = \case
    BootCStandardClang std ->
      "C standard determined by libclang: " >< PP.show std
    BootCStandardFail ->
      "Unable to determine C standard"

instance IsTrace Level BootCStandardMsg where
  getDefaultLogLevel = \case
    BootCStandardClang{} -> Info
    BootCStandardFail    -> Error

  getSource  = const HsBindgen
  getTraceId = const "boot-c-standard"

-- | Boot trace messages
data BootMsg =
    BootBackendConfig        BackendConfigMsg
  | BootMacos                MacosMsg
  | BootBindingSpec          BindingSpecMsg
  | BootDiscover             DiscoverMsg
  | BootClang                ClangMsg
  | BootCStandard            BootCStandardMsg
  | BootExtraClangArgs       ExtraClangArgsMsg
  | BootHashIncludeArg       C.HashIncludeArgMsg
  | BootCompareClangVersions CompareVersionsMsg
  | BootStatus               BootStatusMsg
  | BootCache                (SafeTrace CacheMsg)
  deriving stock (Show, Generic)
  deriving anyclass (PrettyForTrace, IsTrace Level)