hs-bindgen-1.0.0.0: src-internal/HsBindgen/Clang/Discover.hs
module HsBindgen.Clang.Discover (
-- * Types
Paths(..)
, ClangExe
, BuiltinIncDir
-- * Trace messages
, DiscoverMsg(..)
-- * API
, getPaths
, applyPaths
) where
import Control.Exception (Exception (displayException))
import Data.IORef (IORef)
import Data.IORef qualified as IORef
import GHC.Stack (CallStack)
import System.Environment qualified as Env
import System.IO.Unsafe (unsafePerformIO)
import Text.SimplePrettyPrint (string, (<+>))
import Text.SimplePrettyPrint qualified as PP
import Clang.Discover (BuiltinIncDir, ClangExe, Paths (..))
import Clang.Discover qualified as Discover
import HsBindgen.Config.ClangArgs
import HsBindgen.Imports
import HsBindgen.Util.Tracer
{-------------------------------------------------------------------------------
Trace messages
-------------------------------------------------------------------------------}
-- | Trace messages emitted during path discovery
--
-- The actual discovery is performed by @libclang-bindings@
-- ('Discover.getPaths'); its trace messages are wrapped in 'DiscoverClang'. The
-- remaining constructors cover the @hs-bindgen@-specific environment variable
-- check (see 'getEnvConfig').
data DiscoverMsg =
DiscoverEnvNone
| DiscoverEnvSet BuiltinIncDirConfig
| DiscoverEnvInvalid String
| DiscoverClang Discover.DiscoverMsg
deriving stock (Show)
instance PrettyForTrace DiscoverMsg where
prettyForTrace = \case
DiscoverEnvNone ->
PP.string envName <+> "not set"
DiscoverEnvSet config ->
PP.string envName <+> "set:" <+> PP.string (show config)
DiscoverEnvInvalid s ->
PP.string envName <+> "invalid:" <+> PP.string (show s) <+> "(ignored)"
DiscoverClang msg -> prettyDiscoverMsg msg
instance IsTrace Level DiscoverMsg where
getDefaultLogLevel = \case
DiscoverEnvNone -> Debug
DiscoverEnvSet{} -> Info
DiscoverEnvInvalid{} -> Warning
DiscoverClang msg -> discoverMsgLevel msg
getSource = const HsBindgen
getTraceId = const "builtin-include-dir"
-- | Pretty-print an upstream 'Discover.DiscoverMsg'
prettyDiscoverMsg :: Discover.DiscoverMsg -> PP.CtxDoc
prettyDiscoverMsg = \case
Discover.DiscoverLlvmPathNotFound path ->
"$LLVM_PATH path not found or not directory (skipping):" <+> string path
Discover.DiscoverLlvmConfigPathFound path ->
"llvm-config found using $PATH:" <+> string path
Discover.DiscoverLlvmConfigPrefixUnexpected s ->
"llvm-config --prefix output is unexpected:" <+> string (show s)
Discover.DiscoverLlvmConfigPrefixIOError e ->
"IO error calling llvm-config --prefix:" <+> string (displayException e)
Discover.DiscoverClangNotFound ->
"clang not found"
Discover.DiscoverClangVersionMismatch libclangVersion clangVersion ->
PP.hangs' "clang version mismatch:" 2 [
"libclang version:" <+> PP.text libclangVersion
, "clang version: " <+> PP.text clangVersion
]
Discover.DiscoverClangIncDirNotFound path ->
"builtin include directory not found using clang:" <+> string path
Discover.DiscoverClangIncDirFound path ->
"builtin include directory found using clang:" <+> string path
Discover.DiscoverLlvmPathClangExeNotFound path ->
"clang not found using $LLVM_PATH:" <+> string path
Discover.DiscoverLlvmPathClangExeFound path ->
"clang found using $LLVM_PATH:" <+> string path
Discover.DiscoverLlvmConfigClangExeNotFound path ->
"clang not found using llvm-config:" <+> string path
Discover.DiscoverLlvmConfigClangExeFound path ->
"clang found using llvm-config:" <+> string path
Discover.DiscoverClangPathFound path ->
"clang found using $PATH:" <+> string path
Discover.DiscoverClangVersionUnexpected s ->
"clang --version output is unexpected:" <+> string (show s)
Discover.DiscoverClangVersionIOError e ->
"IO error calling clang --version:" <+> string (displayException e)
Discover.DiscoverClangPrintResourceDirUnexpected s ->
"clang -print-resource-dir output is unexpected:" <+> string (show s)
Discover.DiscoverClangPrintResourceDirIOError e ->
"IO error calling clang -print-resource-dir:"
<+> string (displayException e)
-- | Default log level for an upstream 'Discover.DiscoverMsg'
discoverMsgLevel :: Discover.DiscoverMsg -> Level
discoverMsgLevel = \case
Discover.DiscoverLlvmPathNotFound{} -> Warning
Discover.DiscoverLlvmConfigPathFound{} -> Debug
Discover.DiscoverLlvmConfigPrefixUnexpected{} -> Warning
Discover.DiscoverLlvmConfigPrefixIOError{} -> Warning
Discover.DiscoverClangNotFound -> Debug
Discover.DiscoverClangVersionMismatch _ _ -> Warning
Discover.DiscoverClangIncDirNotFound _ -> Warning
Discover.DiscoverClangIncDirFound{} -> Info
Discover.DiscoverLlvmPathClangExeNotFound{} -> Debug
Discover.DiscoverLlvmPathClangExeFound{} -> Debug
Discover.DiscoverLlvmConfigClangExeNotFound{} -> Debug
Discover.DiscoverLlvmConfigClangExeFound{} -> Debug
Discover.DiscoverClangPathFound{} -> Debug
Discover.DiscoverClangVersionUnexpected _ -> Warning
Discover.DiscoverClangVersionIOError _ -> Warning
Discover.DiscoverClangPrintResourceDirUnexpected _ -> Warning
Discover.DiscoverClangPrintResourceDirIOError _ -> Warning
{-------------------------------------------------------------------------------
Global state
-------------------------------------------------------------------------------}
data DiscoverState =
DiscoverStateInitial
| DiscoverStateCached Discover.Paths
-- | Global state for caching discovered paths
--
-- Paths should only be discovered a single time. Calling 'getPaths' stores the
-- result in this global state, and any subsequent calls simply returns the
-- cached value.
discoverState :: IORef DiscoverState
discoverState = unsafePerformIO $ IORef.newIORef DiscoverStateInitial
{-# NOINLINE discoverState #-}
{-------------------------------------------------------------------------------
API
-------------------------------------------------------------------------------}
-- | Try to discover paths for the @clang@ executable, and the builtin include
-- directory
--
-- This wraps 'Discover.getPaths' from @libclang-bindings@, adding two
-- @hs-bindgen@-specific behaviours:
--
-- * The @BINDGEN_BUILTIN_INCLUDE_DIR@ environment variable, when set, overrides
-- the passed 'BuiltinIncDirConfig' (see 'getEnvConfig').
-- * Discovered paths are cached: discovery is only performed a single time, and
-- any subsequent call simply returns the cached value.
--
-- See 'Discover.getPaths' for a description of the discovery strategy.
getPaths ::
Tracer DiscoverMsg
-> BuiltinIncDirConfig
-> IO Discover.Paths
getPaths tracer config =
IORef.readIORef discoverState >>= \case
DiscoverStateCached paths -> return paths
DiscoverStateInitial -> do
mEnvConfig <- getEnvConfig tracer
paths <- toPaths <$>
Discover.getPaths trace (fromMaybe config mEnvConfig)
IORef.writeIORef discoverState (DiscoverStateCached paths)
return paths
where
-- Feed upstream trace messages (paired with their callstack) into our
-- tracer, wrapped in 'DiscoverClang'.
trace :: CallStack -> Discover.DiscoverMsg -> IO ()
trace cs msg = traceWith tracer (WithCallStack cs (DiscoverClang msg))
toPaths :: Discover.Paths -> Discover.Paths
toPaths p = Discover.Paths {
pClangExe = Discover.pClangExe p
, pBuiltinIncDir = Discover.pBuiltinIncDir p
}
-- | Apply the discovered paths to 'Clang.Args.ClangArgs'
--
-- When configured, the builtin include directory is passed with @-isystem@ as
-- the last argument. This ensures that it is prioritized as close to the
-- default include directories as possible.
applyPaths ::
Discover.Paths
-> ClangArgsConfig path
-> ClangArgsConfig path
applyPaths paths = case paths.pBuiltinIncDir of
Nothing -> id
Just builtinIncDir -> #argsAfter %~ (++ ["-isystem", builtinIncDir])
{-------------------------------------------------------------------------------
Auxiliary functions
-------------------------------------------------------------------------------}
-- | Environment variable name
envName :: String
envName = "BINDGEN_BUILTIN_INCLUDE_DIR"
-- | Load configuration from system environment, when available
getEnvConfig :: Tracer DiscoverMsg -> IO (Maybe BuiltinIncDirConfig)
getEnvConfig tracer = Env.lookupEnv envName >>= \case
Nothing -> Nothing <$ traceWith tracer (withCallStack DiscoverEnvNone)
Just "disable" -> aux BuiltinIncDirDisable
Just "clang" -> aux BuiltinIncDirClang
Just s -> Nothing <$ traceWith tracer (withCallStack $ DiscoverEnvInvalid s)
where
aux :: BuiltinIncDirConfig -> IO (Maybe BuiltinIncDirConfig)
aux config = Just config <$ traceWith tracer (withCallStack $ DiscoverEnvSet config)