hs-bindgen-1.0.0.0: app/HsBindgen/App.hs
{-# LANGUAGE ApplicativeDo #-}
-- | @hs-bindgen@ application common types and functions
module HsBindgen.App (
hsBindgen
-- * Global options
, GlobalOpts(..)
, parseGlobalOpts
-- * Argument/option parsers
-- ** Bindgen configuration
, Config
, parseConfig
-- ** Clang arguments
, parseClangArgsConfig
-- ** Translation option
, parseUniqueId
-- ** Module option
, parseBaseModuleName
, parseQualifiedStyle
-- ** Output options
, parseHsOutputDir
, parseDirPolicy
, parseFilePolicy
, parseGenBindingSpec
, parseGenTestsOutput
-- ** Input arguments
, parseInputs
-- * Auxiliary optparse-applicative functions
, cmd
, cmd_
) where
import Data.Char qualified as Char
import Data.Default (Default (..))
import Data.Either (partitionEithers)
import Data.Maybe (catMaybes)
import Options.Applicative
import Options.Applicative.Extra (helperWith)
import System.IO (stderr)
import HsBindgen
import HsBindgen.ArtefactM (DirPolicy (..), FilePolicy (..))
import HsBindgen.BindingSpec
import HsBindgen.Config
import HsBindgen.Config.ClangArgs
import HsBindgen.Config.Internal
import HsBindgen.Frontend.Pass.Parse.IsPass (EmptyMacros (..))
import HsBindgen.Frontend.Pass.Select.IsPass
import HsBindgen.Frontend.Predicate
import HsBindgen.IR.C qualified as C
import HsBindgen.Macro (CExpr)
import HsBindgen.Macro qualified as Macro
import HsBindgen.TraceMsg
import HsBindgen.Util.Tracer
-- | Convenience entry point using the default C macro language.
hsBindgen ::
TracerConfig Level TraceMsg
-> TracerConfig SafeLevel SafeTraceMsg
-> BindgenConfig
-> [C.UncheckedRootDirective]
-> Artefact CExpr a
-> IO a
hsBindgen = hsBindgenMacroLang (pure . Macro.cExpr)
{-------------------------------------------------------------------------------
Global options
-------------------------------------------------------------------------------}
data GlobalOpts = GlobalOpts {
unsafe :: TracerConfig Level TraceMsg
, safe :: TracerConfig SafeLevel SafeTraceMsg
}
parseGlobalOpts :: Parser GlobalOpts
parseGlobalOpts = aux <$> parseAnsiColor <*> parseTracerConfig
where
aux :: Maybe AnsiColor -> TracerConfig Level TraceMsg -> GlobalOpts
aux ansiColor tracerConfig =
let tracerConfigUnsafe :: TracerConfig Level TraceMsg
tracerConfigUnsafe =
tracerConfig { outputConfig = mkOutputConfig ansiColor }
tracerConfigSafe :: TracerConfig SafeLevel SafeTraceMsg
tracerConfigSafe = TracerConfig {
verbosity = tracerConfig.verbosity
, outputConfig = mkOutputConfig ansiColor
, customLogLevel = mempty
, showCallStack = tracerConfig.showCallStack
}
in GlobalOpts tracerConfigUnsafe tracerConfigSafe
{-------------------------------------------------------------------------------
Output configuration
-------------------------------------------------------------------------------}
-- | Output configuration writing to 'stderr' with the given ANSI color setting.
--
-- 'Nothing' auto-detects color support from the handle.
mkOutputConfig :: Maybe AnsiColor -> OutputConfig e
mkOutputConfig ansiColor = OutputConfigHandle OutputHandle {
handle = stderr
, ansiColor = ansiColor
}
-- | Parse the @--color WHEN@ option: 'Nothing' (auto) is the default.
parseAnsiColor :: Parser (Maybe AnsiColor)
parseAnsiColor = option (eitherReader auxParse) $ mconcat [
long "color"
, metavar "WHEN"
, value Nothing
, showDefaultWith auxRender
, help "Colorize diagnostics WHEN (supported: always, auto, never)"
]
where
auxParse :: String -> Either String (Maybe AnsiColor)
auxParse = \case
"always" -> Right (Just EnableAnsiColor)
"auto" -> Right Nothing
"never" -> Right (Just DisableAnsiColor)
other -> Left $
"invalid color mode: " ++ other ++ " (supported: always, auto, never)"
auxRender :: Maybe AnsiColor -> String
auxRender = \case
Just EnableAnsiColor -> "always"
Nothing -> "auto"
Just DisableAnsiColor -> "never"
{-------------------------------------------------------------------------------
Tracer configuration
-------------------------------------------------------------------------------}
parseTracerConfig :: Parser (TracerConfig Level TraceMsg)
parseTracerConfig =
TracerConfig
<$> parseVerbosity
<*> pure def
<*> parseCustomLogLevel
<*> parseShowCallStack
parseVerbosity :: Parser Verbosity
parseVerbosity = fmap nToVerbosity . option auto $ mconcat [
short 'v'
, long "verbosity"
, metavar "INT"
, value 2
, help "Verbosity (0: error, 1: warning, 2: notice, 3: info, 4: debug)"
, showDefault
]
where
nToVerbosity :: Int -> Verbosity
nToVerbosity = Verbosity . \case
n | n <= 0 -> Error
1 -> Warning
2 -> Notice
3 -> Info
_otherwise -> Debug
parseCustomLogLevel :: Parser (CustomLogLevel Level TraceMsg)
parseCustomLogLevel = do
-- Generic setters
makeTraceInfos <- many $ parseMakeTrace Info
makeTraceWarnings <- many $ parseMakeTrace Warning
makeTraceErrors <- many $ parseMakeTrace Error
-- Generic modifiers
makeBugsErrors <- optional parseMakeBugsErrors
makeWarningsErrors <- optional parseMakeWarningsErrors
-- Specific setters
enableMacroWarnings <- optional parseEnableMacroWarnings
makeMangleNamesSquashedInfo <- optional parseMakeMangleNamesSquashedInfo
pure $ getCustomLogLevel $ catMaybes [
enableMacroWarnings
, makeMangleNamesSquashedInfo
, makeBugsErrors
, makeWarningsErrors
]
++ makeTraceInfos
++ makeTraceWarnings
++ makeTraceErrors
where
parseMakeTrace :: Level -> Parser CustomLogLevelSetting
parseMakeTrace level =
let levelStr = map Char.toLower $ show level
in fmap (MakeTrace level) . strOption $ mconcat [
long $ "log-as-" <> levelStr
, metavar "TRACE_ID"
, help $ "Set log level of traces with TRACE_ID to " <> levelStr
]
parseMakeBugsErrors :: Parser CustomLogLevelSetting
parseMakeBugsErrors = flag' MakeBugsErrors $ mconcat [
long "log-as-error-bugs"
, help "Set log level of bugs to error"
]
parseMakeWarningsErrors :: Parser CustomLogLevelSetting
parseMakeWarningsErrors = flag' MakeWarningsErrors $ mconcat [
long "log-as-error-warnings"
, help "Set log level of warnings and bugs to error"
]
parseEnableMacroWarnings :: Parser CustomLogLevelSetting
parseEnableMacroWarnings = flag' EnableMacroWarnings $ mconcat [
long "log-enable-macro-warnings"
, help $ concat [
"Set log level of macro reparse and typecheck errors to warning"
, " (default: info)"
]
]
parseMakeMangleNamesSquashedInfo :: Parser CustomLogLevelSetting
parseMakeMangleNamesSquashedInfo =
flag' MakeMangleNamesSquashedInfo $ mconcat [
long "log-squashed-as-info"
, help $ concat [
"Set log level of squashed-typedef traces to info"
, " (default: notice)"
]
]
parseShowCallStack :: Parser ShowCallStack
parseShowCallStack = flag DisableCallStack EnableCallStack $ mconcat [
long "log-show-call-stack"
, help "Show call stacks in traces"
]
{-------------------------------------------------------------------------------
Configuration
-------------------------------------------------------------------------------}
type Config = Config_ FilePath
parseConfig :: Parser Config
parseConfig = Config
<$> parseClangArgsConfig
<*> parseBindingSpec
<*> parseSelectionPredicate
<*> parseProgramSlicing
<*> parseFieldNamingStrategy
<*> parseEmptyMacros
{-------------------------------------------------------------------------------
Binding specifications
-------------------------------------------------------------------------------}
parseBindingSpec :: Parser BindingSpecConfig
parseBindingSpec = BindingSpecConfig
<$> parseEnableStdlibBindingSpec
<*> parseBindingSpecAllowNewer
<*> many parseExtBindingSpec
<*> optional parsePrescriptiveBindingSpec
parseEnableStdlibBindingSpec :: Parser EnableStdlibBindingSpec
parseEnableStdlibBindingSpec =
flag EnableStdlibBindingSpec DisableStdlibBindingSpec $ mconcat [
long "no-stdlib"
, help "Do not automatically use stdlib external binding specification"
]
parseBindingSpecAllowNewer :: Parser BindingSpecCompatibility
parseBindingSpecAllowNewer =
flag BindingSpecStrict BindingSpecAllowNewer $ mconcat [
long "binding-spec-allow-newer"
, help "Parse binding specifications with newer minor version"
]
parseExtBindingSpec :: Parser FilePath
parseExtBindingSpec = strOption $ mconcat [
long "external-binding-spec"
, metavar "FILE"
, help "External binding specification (YAML file)"
]
parsePrescriptiveBindingSpec :: Parser FilePath
parsePrescriptiveBindingSpec = strOption $ mconcat [
long "prescriptive-binding-spec"
, metavar "FILE"
, help "Prescriptive binding specification (YAML file)"
]
{-------------------------------------------------------------------------------
Builtin include directory
-------------------------------------------------------------------------------}
parseBuiltinIncDirConfig :: Parser BuiltinIncDirConfig
parseBuiltinIncDirConfig = option (eitherReader auxParse) $ mconcat [
long "builtin-include-dir"
, metavar "MODE"
, showDefaultWith auxRender
, value BuiltinIncDirClang
, help
"Configure builtin include directory (supported: clang, disable)"
]
where
auxParse :: String -> Either String BuiltinIncDirConfig
auxParse = \case
"clang" -> Right BuiltinIncDirClang
"disable" -> Right BuiltinIncDirDisable
other -> Left $ "invalid builtin include directory mode: " ++ other
auxRender :: BuiltinIncDirConfig -> String
auxRender = \case
BuiltinIncDirClang -> "clang"
BuiltinIncDirDisable -> "disable"
{-------------------------------------------------------------------------------
Clang arguments
-------------------------------------------------------------------------------}
parseClangArgsConfig :: Parser (ClangArgsConfig FilePath)
parseClangArgsConfig = do
-- ApplicativeDo to be able to reorder arguments for --help, and to use
-- record construction (i.e., to avoid bool or string/path blindness)
-- instead of positional one.
enableBlocks <- parseEnableBlocks
builtinIncDir <- parseBuiltinIncDirConfig
extraIncludeDirs <- many parseIncludeDir
argsBefore <- many parseClangOptionBefore
argsInner <- many parseClangOptionInner
argsAfter <- many parseClangOptionAfter
pure $ ClangArgsConfig{
enableBlocks = enableBlocks
, builtinIncDir = builtinIncDir
, extraIncludeDirs = extraIncludeDirs
, argsBefore = argsBefore
, argsInner = argsInner
, argsAfter = argsAfter
}
parseEnableBlocks :: Parser Bool
parseEnableBlocks = switch $ mconcat [
long "fblocks"
, help "Enable the 'blocks' language feature"
]
parseIncludeDir :: Parser FilePath
parseIncludeDir = strOption $ mconcat [
short 'I'
, metavar "DIR"
, help "Include search path directory"
]
parseClangOptionBefore :: Parser String
parseClangOptionBefore = strOption $ mconcat [
long "clang-option-before"
, metavar "OPTION"
, help "Prepend option when calling Clang; see also --clang-option"
]
parseClangOptionInner :: Parser String
parseClangOptionInner = strOption $ mconcat [
long "clang-option"
, metavar "OPTION"
, help "Pass option to Clang"
]
parseClangOptionAfter :: Parser String
parseClangOptionAfter = strOption $ mconcat [
long "clang-option-after"
, metavar "OPTION"
, help "Append option when calling Clang; see also --clang-option"
]
{-------------------------------------------------------------------------------
Predicates and slicing
-------------------------------------------------------------------------------}
parseSelectionPredicate :: Parser (Boolean SelectionPredicate)
parseSelectionPredicate = fmap aux . many . asum $ [
flag' (Right BTrue) $ mconcat [
long "select-all"
, help "Select all declarations"
]
, flag' (Right (BIf (SelectHeader FromMainHeaders))) $ mconcat [
long "select-from-main-headers"
, help "Select declarations in main headers (default)"
]
, flag' (Right (BIf (SelectHeader FromMainHeaderDirs))) $ mconcat [
long "select-from-main-header-dirs"
, help "Select declarations in main header directories"
]
, flag' (Right (BIf (SelectHeader FromAllHeaders))) $ mconcat [
long "select-from-all-headers"
, help $ concat [
"Select declarations in any header"
, " (unlike --select-all, not root directives or -D options)"
]
]
, fmap (Right . BIf . SelectHeader . HeaderPathMatches) $ strOption $ mconcat [
long "select-by-header-path"
, metavar "PCRE"
, help "Select declarations in headers with paths that match PCRE"
]
, fmap (Left . BIf . SelectHeader . HeaderPathMatches) $ strOption $ mconcat [
long "select-except-by-header-path"
, metavar "PCRE"
, help $ concat [
"Select except declarations in headers with paths that match"
, " PCRE"
]
]
, fmap (Right . BIf . SelectDecl . DeclNameMatches) $ strOption $ mconcat [
long "select-by-decl-name"
, metavar "PCRE"
, help "Select declarations with C names that match PCRE"
]
, fmap (Left . BIf . SelectDecl . DeclNameMatches) $ strOption $ mconcat [
long "select-except-by-decl-name"
, metavar "PCRE"
, help "Select except (i.e., do not select) declarations with C names that match PCRE"
]
, flag' (Left $ BIf $ SelectDecl DeclDeprecated) $ mconcat [
long "select-except-deprecated"
, help "Select except (i.e., do not select) deprecated declarations"
]
]
where
aux ::
[Either (Boolean SelectionPredicate) (Boolean SelectionPredicate)]
-> (Boolean SelectionPredicate)
aux = uncurry mergeBooleans . fmap applyDefault . partitionEithers
applyDefault :: Default a => [a] -> [a]
applyDefault = \case
[] -> [def]
ps -> ps
parseProgramSlicing :: Parser ProgramSlicing
parseProgramSlicing =
flag DisableProgramSlicing EnableProgramSlicing $ mconcat [
long "enable-program-slicing"
, help $ concat [
"Enable program slicing:"
, " Select declarations using the selection predicate,"
, " and also select their transitive dependencies;"
, " program slicing can cause declarations to be included"
, " even if they are explicitly deselected by a selection predicate"
]
]
{-------------------------------------------------------------------------------
Macros
-------------------------------------------------------------------------------}
parseEmptyMacros :: Parser EmptyMacros
parseEmptyMacros =
flag DoNotParseEmptyMacros ParseEmptyMacros $ mconcat [
long "parse-empty-macros"
, help $ concat [
"Parse macros with an empty replacement list (e.g. '#define FOO');"
, " by default, empty macros are not parsed"
]
]
{-------------------------------------------------------------------------------
Translation options
-------------------------------------------------------------------------------}
parseUniqueId :: Parser UniqueId
parseUniqueId = fmap UniqueId . strOption $ mconcat [
long "unique-id"
, metavar "ID"
, value ""
, help $ concat [
"Use unique ID to discriminate global C identifiers"
, " (default: empty string)"
]
]
{-------------------------------------------------------------------------------
Module option
-------------------------------------------------------------------------------}
parseFieldNamingStrategy :: Parser FieldNamingStrategy
parseFieldNamingStrategy =
flag AddFieldPrefixes OmitFieldPrefixes $ mconcat [
long "omit-field-prefixes"
, help $ concat [
"Use unprefixed field names (e.g. 'x' instead of 'structName_x'). "
, "All newtype unwrap functions are called 'unwrap'. "
, "Requires 'DuplicateRecordFields' extension."
]
]
parseQualifiedStyle :: Parser QualifiedStyle
parseQualifiedStyle =
flag PreQualified PostQualified $ mconcat [
long "post-qualified-imports"
, help $ concat [
"Use post-qualified imports (e.g. 'import Data.Proxy qualified')"
, " instead of pre-qualified imports."
, " Adds 'ImportQualifiedPost' extension."
]
]
parseBaseModuleName :: Parser BaseModuleName
parseBaseModuleName = strOption $ mconcat [
long "module"
, metavar "NAME"
, showDefault
, value def
, help "Base name of the generated Haskell modules"
]
{-------------------------------------------------------------------------------
Output options
-------------------------------------------------------------------------------}
parseHsOutputDir :: Parser FilePath
parseHsOutputDir = strOption $ mconcat [
long "hs-output-dir"
, metavar "PATH"
, help "Output directory of generated Haskell modules"
]
parseDirPolicy :: Parser DirPolicy
parseDirPolicy = flag DoNotCreateOutputDirs CreateOutputDirs $ mconcat [
long "create-output-dirs"
, help "Create the specified output directories if they do not exist"
]
parseFilePolicy :: Parser FilePolicy
parseFilePolicy = flag DoNotOverwriteFiles AllowFileOverwrite $ mconcat [
long "overwrite-files"
, help "Allow overwriting existing output files"
]
parseGenBindingSpec :: Parser FilePath
parseGenBindingSpec = strOption $ mconcat [
long "gen-binding-spec"
, metavar "PATH"
, help "Binding specification to generate"
]
parseGenTestsOutput :: Parser FilePath
parseGenTestsOutput = strOption $ mconcat [
short 'o'
, long "output"
, metavar "PATH"
, showDefault
, value "test-hs-bindgen"
, help "Output directory for the test suite"
]
{-------------------------------------------------------------------------------
Input arguments
-------------------------------------------------------------------------------}
-- | Parse one or more input root directives
--
-- @#include@s and @#define@s form a single ordered list: a @#define@ only
-- affects the headers listed after it.
--
-- We check that at least one directive is of type @#include@ when the root
-- header is constructed ('HsBindgen.Frontend.RootHeader.fromRootDirectives').
-- This ensures that CLI and TH mode have the same behavior. Also,
-- @optparse-applicative@ can only reject the parsed list with monadic
-- sequencing, but its usage renderer does not distinguish monadic sequencing
-- from repetition, so every bind picks up the @multiSuffix@ marker.
parseInputs :: Parser [C.UncheckedRootDirective]
parseInputs = some . asum $ [
C.DirectiveHashInclude <$> parseHashInclude
, C.DirectiveHashDefine <$> parseHashDefine
]
where
parseHashInclude :: Parser C.UncheckedHashIncludeArg
parseHashInclude = strArgument $ mconcat [
metavar "HEADER"
, help "Input C header(s), relative to an include path directory"
]
parseHashDefine :: Parser C.HashDefine
parseHashDefine =
C.HashDefine
-- The metavar covers both arguments, so that @--help@ shows the
-- @#define@ shape; the value argument itself renders nothing.
<$> strOption (long "hash-define" <> metavar "NAME VALUE" <> help hashDefineHelp)
<*> strArgument mempty
hashDefineHelp :: String
hashDefineHelp = unwords [
"Emit '#define NAME VALUE' before the following headers."
, "See 'Root directives' in the manual"
, "(manual/low-level/usage/c-stages.md)."
]
{-------------------------------------------------------------------------------
Auxiliary optparse-applicative functions
-------------------------------------------------------------------------------}
-- | Command with @-h@ and @--help@
cmd ::
String -- ^ Name
-> (a -> c) -- ^ Constructor
-> Parser a -- ^ Options parser
-> InfoMod c -- ^ Information
-> Mod CommandFields c
cmd name mk parser = command name . info (helper <*> (mk <$> parser))
-- | Command with @--help@ (but no @-h@)
cmd_ ::
String -- ^ Name
-> (a -> c) -- ^ Constructor
-> Parser a -- ^ Options parser
-> InfoMod c -- ^ Information
-> Mod CommandFields c
cmd_ name mk parser = command name . info (helper' <*> (mk <$> parser))
where
helper' :: Parser (a -> a)
helper' = helperWith $ mconcat [
long "help"
, help "Show this help text"
]