haskell-gi-0.15: lib/Data/GI/CodeGen/Overrides.hs
{-# LANGUAGE ViewPatterns #-}
module Data.GI.CodeGen.Overrides
( Overrides(pkgConfigMap, cabalPkgVersion, nsChooseVersion, girFixups)
, parseOverridesFile
, filterAPIsAndDeps
) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
import Data.Traversable (traverse)
#endif
import Control.Monad.Except
import Control.Monad.State
import Control.Monad.Writer
import Data.Maybe (isJust)
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Text (Text)
import qualified System.Info as SI
import qualified System.Environment as SE
import Data.GI.CodeGen.API
import qualified Text.XML as XML
import Data.GI.GIR.XMLUtils (xmlLocalName, xmlNSName,
GIRXMLNamespace(CGIRNS, GLibGIRNS))
data Overrides = Overrides {
-- | Ignored elements of a given API.
ignoredElems :: M.Map Name (S.Set Text),
-- | Ignored APIs (all elements in this API will just be discarded).
ignoredAPIs :: S.Set Name,
-- | Structs for which accessors should not be auto-generated.
sealedStructs :: S.Set Name,
-- | Explicit calloc/copy/free for structs/unions.
allocInfo :: M.Map Name AllocationInfo,
-- | Mapping from GObject Introspection namespaces to pkg-config
pkgConfigMap :: M.Map Text Text,
-- | Version number for the generated .cabal package.
cabalPkgVersion :: Maybe Text,
-- | Prefered version of the namespace.
nsChooseVersion :: M.Map Text Text,
-- | Fixups for the GIR data.
girFixups :: [GIRRule]
} deriving (Show)
-- | Construct the generic config for a module.
defaultOverrides :: Overrides
defaultOverrides = Overrides {
ignoredElems = M.empty,
ignoredAPIs = S.empty,
sealedStructs = S.empty,
allocInfo = M.empty,
pkgConfigMap = M.empty,
cabalPkgVersion = Nothing,
nsChooseVersion = M.empty,
girFixups = []
}
-- | There is a sensible notion of zero and addition of Overridess,
-- encode this so that we can view the parser as a writer monad of
-- configs.
instance Monoid Overrides where
mempty = defaultOverrides
mappend a b = Overrides {
ignoredAPIs = ignoredAPIs a <> ignoredAPIs b,
sealedStructs = sealedStructs a <> sealedStructs b,
allocInfo = allocInfo a <> allocInfo b,
ignoredElems = M.unionWith S.union (ignoredElems a) (ignoredElems b),
pkgConfigMap = pkgConfigMap a <> pkgConfigMap b,
cabalPkgVersion = if isJust (cabalPkgVersion b)
then cabalPkgVersion b
else cabalPkgVersion a,
nsChooseVersion = nsChooseVersion a <> nsChooseVersion b,
girFixups = girFixups a <> girFixups b
}
-- | The state of the overrides parser.
data ParserState = ParserState {
currentNS :: Maybe Text -- ^ The current namespace.
, flags :: [ParserFlag] -- ^ Currently loaded flags.
} deriving (Show)
-- | Default, empty, parser state.
emptyParserState :: ParserState
emptyParserState = ParserState {
currentNS = Nothing
, flags = []
}
-- | Conditional flags for the parser
data ParserFlag = FlagLinux
| FlagOSX
| FlagWindows
deriving (Show)
-- | Get the current namespace.
getNS :: Parser (Maybe Text)
getNS = currentNS <$> get
-- | Run the given parser only if the flags can be satisfied.
withFlags :: Parser () -> Parser ()
withFlags p = do
fs <- flags <$> get
check <- and <$> liftIO (traverse checkFlag fs)
if check
then p
else return ()
-- | Check whether the given flag holds.
checkFlag :: ParserFlag -> IO Bool
checkFlag FlagLinux = checkOS "linux"
checkFlag FlagOSX = checkOS "darwin"
checkFlag FlagWindows = checkOS "mingw32"
-- | Check whether we are running under the given OS. We take the OS
-- from `System.Info.os`, but it is possible to override this value by
-- setting the environment variable @HASKELL_GI_OVERRIDE_OS@.
checkOS :: String -> IO Bool
checkOS os = SE.lookupEnv "HASKELL_GI_OVERRIDE_OS" >>= \case
Nothing -> return (SI.os == os)
Just ov -> return (ov == os)
-- | We have a bit of context (the current namespace), and can fail,
-- encode this in a monad.
type Parser a = WriterT Overrides (StateT ParserState (ExceptT Text IO)) a
-- | Parse the given config file (as a set of lines) for a given
-- introspection namespace, filling in the configuration as needed. In
-- case the parsing fails we return a description of the error
-- instead.
parseOverridesFile :: [Text] -> IO (Either Text Overrides)
parseOverridesFile ls =
runExceptT $ flip evalStateT emptyParserState $ execWriterT $
mapM (parseOneLine . T.strip) ls
-- | Parse a single line of the config file, modifying the
-- configuration as appropriate.
parseOneLine :: Text -> Parser ()
-- Empty lines
parseOneLine line | T.null line = return ()
-- Comments
parseOneLine (T.stripPrefix "#" -> Just _) = return ()
parseOneLine (T.stripPrefix "namespace " -> Just ns) =
withFlags $ modify' (\s -> s {currentNS = (Just . T.strip) ns})
parseOneLine (T.stripPrefix "ignore " -> Just ign) =
withFlags $ getNS >>= parseIgnore ign
parseOneLine (T.stripPrefix "seal " -> Just s) =
withFlags $ getNS >>= parseSeal s
parseOneLine (T.stripPrefix "alloc-info " -> Just s) =
withFlags $ getNS >>= parseAllocInfo s
parseOneLine (T.stripPrefix "pkg-config-name " -> Just s) =
withFlags $ parsePkgConfigName s
parseOneLine (T.stripPrefix "cabal-pkg-version " -> Just s) =
withFlags $ parseCabalPkgVersion s
parseOneLine (T.stripPrefix "namespace-version " -> Just s) =
withFlags $ parseNsVersion s
parseOneLine (T.stripPrefix "set-attr " -> Just s) =
withFlags $ parseSetAttr s
parseOneLine (T.stripPrefix "if " -> Just s) =
withFlags $ parseIf s
parseOneLine (T.stripPrefix "endif" -> Just s) = parseEndif s
parseOneLine l = throwError $ "Could not understand \"" <> l <> "\"."
-- | Ignored elements.
parseIgnore :: Text -> Maybe Text -> Parser ()
parseIgnore _ Nothing =
throwError "'ignore' requires a namespace to be defined first."
parseIgnore (T.words -> [T.splitOn "." -> [api,elem]]) (Just ns) =
tell $ defaultOverrides {ignoredElems = M.singleton (Name ns api)
(S.singleton elem)}
parseIgnore (T.words -> [T.splitOn "." -> [api]]) (Just ns) =
tell $ defaultOverrides {ignoredAPIs = S.singleton (Name ns api)}
parseIgnore ignore _ =
throwError ("Ignore syntax is of the form \"ignore API.elem\" with '.elem' optional.\nGot \"ignore " <> ignore <> "\" instead.")
-- | Sealed structures.
parseSeal :: Text -> Maybe Text -> Parser ()
parseSeal _ Nothing = throwError "'seal' requires a namespace to be defined first."
parseSeal (T.words -> [s]) (Just ns) = tell $
defaultOverrides {sealedStructs = S.singleton (Name ns s)}
parseSeal seal _ =
throwError ("seal syntax is of the form \"seal name\".\nGot \"seal "
<> seal <> "\" instead.")
-- | Explicit allocation info for wrapped pointers.
parseAllocInfo :: Text -> Maybe Text -> Parser ()
parseAllocInfo _ Nothing = throwError "'alloc-info' requires a namespace to be defined first."
parseAllocInfo (T.words -> (n:ops)) (Just ns) = do
parsedOps <- traverse parseKeyValuePair ops
info <- foldM applyOp unknownAllocationInfo parsedOps
tell $ defaultOverrides {allocInfo = M.singleton (Name ns n) info}
where applyOp :: AllocationInfo -> (Text, Text) -> Parser AllocationInfo
applyOp a ("calloc", f) = return (a {allocCalloc = AllocationOp f})
applyOp a ("copy", f) = return (a {allocCopy = AllocationOp f})
applyOp a ("free", f) = return (a {allocFree = AllocationOp f})
applyOp _ (op, _) = throwError ("Unknown alloc op \"" <> op <> "\".")
parseAllocInfo info _ =
throwError ("alloc-info syntax is of the form "
<> "\"alloc-info name calloc copy free\", with \"-\" meaning "
<> "a masked operation. Got \"alloc-info " <> info
<> "\" instead.")
-- | Parse a explicit key=value pair into a (key, value) tuple.
parseKeyValuePair :: Text -> Parser (Text, Text)
parseKeyValuePair p =
case T.splitOn "=" p of
[k,v] -> return (k, v)
_ -> throwError ("Could not parse \"" <> p <> "\"as a \"key=value\" pair.")
-- | Mapping from GObject Introspection namespaces to pkg-config.
parsePkgConfigName :: Text -> Parser ()
parsePkgConfigName (T.words -> [gi,pc]) = tell $
defaultOverrides {pkgConfigMap =
M.singleton (T.toLower gi) pc}
parsePkgConfigName t =
throwError ("pkg-config-name syntax is of the form\n" <>
"\t\"pkg-config-name gi-namespace pk-name\"\n" <>
"Got \"pkg-config-name " <> t <> "\" instead.")
-- | Choose a preferred namespace version to load.
parseNsVersion :: Text -> Parser ()
parseNsVersion (T.words -> [ns,version]) = tell $
defaultOverrides {nsChooseVersion =
M.singleton ns version}
parseNsVersion t =
throwError ("namespace-version syntax is of the form\n" <>
"\t\"namespace-version namespace version\"\n" <>
"Got \"namespace-version " <> t <> "\" instead.")
-- | Specifying the cabal package version by hand.
parseCabalPkgVersion :: Text -> Parser ()
parseCabalPkgVersion (T.words -> [version]) = tell $
defaultOverrides {cabalPkgVersion = Just version}
parseCabalPkgVersion t =
throwError ("cabal-pkg-version syntax is of the form\n" <>
"\t\"cabal-pkg-version version\"\n" <>
"Got \"cabal-pkg-version " <> t <> "\" instead.")
-- | Set a given attribute in the GIR file.
parseSetAttr :: Text -> Parser ()
parseSetAttr (T.words -> [path, attr, newVal]) = do
pathSpec <- parsePathSpec path
parsedAttr <- parseXMLName attr
tell $ defaultOverrides {girFixups =
[GIRSetAttr (pathSpec, parsedAttr) newVal]}
parseSetAttr t =
throwError ("set-attr syntax is of the form\n" <>
"\t\"set-attr nodePath attrName newValue\"\n" <>
"Got \"set-attr " <> t <> "\" instead.")
-- | Parse a path specification, which is of the form
-- "nodeSpec1/nodeSpec2/../nodeSpecN", where nodeSpec is a node
-- specification of the form "nodeType[:name attribute]".
parsePathSpec :: Text -> Parser GIRPath
parsePathSpec spec = mapM parseNodeSpec (T.splitOn "/" spec)
-- | Parse a single node specification.
parseNodeSpec :: Text -> Parser GIRNodeSpec
parseNodeSpec spec = case T.splitOn "@" spec of
[n] -> return (GIRNamed n)
["", t] -> return (GIRType t)
[n, t] -> return (GIRTypedName t n)
_ -> throwError ("Could not understand node spec \""
<> spec <> "\".")
-- | Parse an XML name, with an optional prefix.
parseXMLName :: Text -> Parser XML.Name
parseXMLName a = case T.splitOn ":" a of
[n] -> return (xmlLocalName n)
["c", n] -> return (xmlNSName CGIRNS n)
["glib", n] -> return (xmlNSName GLibGIRNS n)
_ -> throwError ("Could not understand xml name \""
<> a <> "\".")
-- | Parse a 'if' directive.
parseIf :: Text -> Parser ()
parseIf cond = case T.words cond of
[] -> throwError ("Empty 'if' condition.")
["linux"] -> setFlag FlagLinux
["osx"] -> setFlag FlagOSX
["windows"] -> setFlag FlagWindows
_ -> throwError ("Unknown condition \"" <> cond <> "\".")
where setFlag :: ParserFlag -> Parser ()
setFlag flag = modify' (\s -> s {flags = flag : flags s})
-- | Parse an 'endif' directive.
parseEndif :: Text -> Parser ()
parseEndif rest = case T.words rest of
[] -> unsetFlag
_ -> throwError ("Unexpected argument to 'endif': \""
<> rest <> "\".")
where unsetFlag :: Parser ()
unsetFlag = do
s <- get
case flags s of
_:rest -> put (s {flags = rest})
[] -> throwError ("'endif' with no matching 'if'.")
-- | Filter a set of named objects based on a lookup list of names to
-- ignore.
filterMethods :: [Method] -> S.Set Text -> [Method]
filterMethods set ignores =
filter ((`S.notMember` ignores) . name . methodName) set
-- | Given the previous allocation info, and a new allocation info,
-- replace those entries in the old allocation info which are
-- specified in the new info.
filterAllocInfo :: AllocationInfo -> AllocationInfo -> AllocationInfo
filterAllocInfo old new =
AllocationInfo { allocCalloc = replace (allocCalloc old) (allocCalloc new)
, allocCopy = replace (allocCopy old) (allocCopy new)
, allocFree = replace (allocFree old) (allocFree new) }
where replace :: AllocationOp -> AllocationOp -> AllocationOp
replace o AllocationOpUnknown = o
replace _ o = o
-- | Filter one API according to the given config.
filterOneAPI :: Overrides -> (Name, API, Maybe (S.Set Text)) -> (Name, API)
filterOneAPI ovs (n, APIStruct s, maybeIgnores) =
(n, APIStruct s { structMethods = maybe (structMethods s)
(filterMethods (structMethods s))
maybeIgnores
, structFields = if n `S.member` sealedStructs ovs
then []
else structFields s
, structAllocationInfo =
let ai = structAllocationInfo s
in case M.lookup n (allocInfo ovs) of
Just info -> filterAllocInfo ai info
Nothing -> ai
})
filterOneAPI ovs (n, APIUnion u, maybeIgnores) =
(n, APIUnion u {unionMethods = maybe (unionMethods u)
(filterMethods (unionMethods u))
maybeIgnores
, unionAllocationInfo =
let ai = unionAllocationInfo u
in case M.lookup n (allocInfo ovs) of
Just info -> filterAllocInfo ai info
Nothing -> ai
})
-- The rest only apply if there are ignores.
filterOneAPI _ (n, api, Nothing) = (n, api)
filterOneAPI _ (n, APIObject o, Just ignores) =
(n, APIObject o {objMethods = filterMethods (objMethods o) ignores,
objSignals = filter ((`S.notMember` ignores) . sigName)
(objSignals o)
})
filterOneAPI _ (n, APIInterface i, Just ignores) =
(n, APIInterface i {ifMethods = filterMethods (ifMethods i) ignores,
ifSignals = filter ((`S.notMember` ignores) . sigName)
(ifSignals i)
})
filterOneAPI _ (n, api, _) = (n, api)
-- | Given a list of APIs modify them according to the given config.
filterAPIs :: Overrides -> [(Name, API)] -> [(Name, API)]
filterAPIs ovs apis = map (filterOneAPI ovs . fetchIgnores) filtered
where filtered = filter ((`S.notMember` ignoredAPIs ovs) . fst) apis
fetchIgnores (n, api) = (n, api, M.lookup n (ignoredElems ovs))
-- | Load a given API, applying filtering. Load also any necessary
-- dependencies.
filterAPIsAndDeps :: Overrides -> GIRInfo -> [GIRInfo]
-> (M.Map Name API, M.Map Name API)
filterAPIsAndDeps ovs doc deps =
let toMap = M.fromList . filterAPIs ovs . girAPIs
in (toMap doc, M.unions (map toMap deps))