libclang-bindings-0.1.0.0: src/Clang/Version/Internal.hs
{-# LANGUAGE TemplateHaskell #-}
module Clang.Version.Internal (
-- * Definition
ClangVersion(..)
, parseClangVersion
-- * CLANG_VERSION macro
, checkUserClangVersion
) where
import Control.Monad (unless)
import Data.Char qualified as Char
import Data.Maybe
import Data.Text (Text)
import Data.Text qualified as Text
import Foreign.C.String qualified as C
import Language.Haskell.TH.Syntax qualified as TH
import Text.Read (readMaybe)
import Version_libclang_bindings (clangVersionCompileTime)
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Clang version
--
-- We're intentionally /not/ deriving 'Ord', to ensure that version comparisons
-- take 'ClangVersionUnknown' into account.
data ClangVersion =
-- | Parsed version number (major, minor, patch)
ClangVersion (Int, Int, Int)
-- | Unknown version
--
-- We get the version by parsing the result of @clang_getClangVersion@,
-- which explicitly says
--
-- > Return a version string, suitable for showing to a user, but not
-- > intended to be parsed (the format is not guaranteed to be stable).
--
-- Unfortunately, @libclang@ does not provide any other means of getting
-- the version number. We therefore parse the string anyway, and use
-- 'ClangVersionUnknown' when that fails.
--
-- NOTE: While @Index.h@ does provide @CINDEX_VERSION_MAJOR@ (which is
-- always zero) and @CINDEX_VERSION_MINOR@, unfortunately they do not map
-- cleanly to @clang@ versions, and do not provide sufficient resolution.
-- For example, a @CINDEX_VERSION_MINOR@ value of 64 could be any version
-- between @17.0.0@ and @20.1.7@ (and possibly more still).
| ClangVersionUnknown Text
deriving stock (Show, Eq) -- No 'Ord'
-- | Parse clang version string
--
-- @clang_getClangVersion@ may return something like
--
-- > "Ubuntu clang version 14.0.0-1ubuntu1.1"
-- > "Ubuntu clang version 18.1.3 (1ubuntu1)"
-- > "Ubuntu clang version 19.1.1 (1ubuntu1~24.04.2)"
-- > "clang version 14.0.6"
-- > "clang version 14.0.6 (git@github.com:llvm/llvm-project.git f28c006a5895fc0e329fe15fead81e37457cb1d1)"
-- > "clang version 18.1.8 (https://github.com/llvm/llvm-project.git ad36915a8c42d51218eee4b53f2c0aae80eb17e9)"
-- > "clang version 20.1.4 (https://github.com/llvm/llvm-project ec28b8f9cc7f2ac187d8a617a6d08d5e56f9120e)"
--
-- We try to find the proper version (14.0.0, 18.1.3, ..) in this string.
parseClangVersion :: Text -> ClangVersion
parseClangVersion versionString =
maybe (ClangVersionUnknown versionString) ClangVersion
. (>>= readParts)
. listToMaybe
. mapMaybe exactlyThree
. map (splitOn '.' . takeWhile isPartOfVersionProper)
. words
$ Text.unpack versionString
where
isPartOfVersionProper :: Char -> Bool
isPartOfVersionProper c = (c >= '0' && c <= '9') || (c == '.')
exactlyThree :: [a] -> Maybe (a, a, a)
exactlyThree [x, y, z] = Just (x, y, z)
exactlyThree _otherwise = Nothing
readParts :: (String, String, String) -> Maybe (Int, Int, Int)
readParts (x, y, z) = (,,) <$> readMaybe x <*> readMaybe y <*> readMaybe z
{-------------------------------------------------------------------------------
Auxiliary
-------------------------------------------------------------------------------}
-- | Split on every occurrence of the separator
--
-- > splitOn '.' "18.1.3" == ["18","1","3"]
splitOn :: forall a. Eq a => a -> [a] -> [[a]]
splitOn sep = go
where
go :: [a] -> [[a]]
go [] = []
go xs = case break (== sep) xs of
(pref, []) -> [pref]
(pref, _sep:rest) -> pref : go rest
{-------------------------------------------------------------------------------
CLANG_VERSION macro
-------------------------------------------------------------------------------}
-- There is no way to portably stringify a preprocessor macro in GHC. (See
-- <https://gitlab.haskell.org/ghc/ghc/-/issues/12516>.) To work around this,
-- we stringify the macro in C and define a C function that returns the string.
$([] <$ TH.addForeignSource TH.LangC (unlines
[ "#define LCB_STR_HELPER(s) #s"
, "#define LCB_STR(s) LCB_STR_HELPER(s)"
, "const char *user_clang_version ="
, "#ifdef CLANG_VERSION"
, " LCB_STR(CLANG_VERSION);"
, "#else"
, " \"CLANG_VERSION_NOT_SET\";"
, "#endif"
, "const char *get_user_clang_version(void) {"
, " return user_clang_version;"
, "}"
]))
foreign import ccall unsafe "get_user_clang_version"
get_user_clang_version :: IO C.CString
-- | Clang version specified in the @CLANG_VERSION@ macro
data UserClangVersion =
-- | Parsed version
--
-- This includes the original string (for error messages) as well as the
-- major, minor (optional), and patch (optional) numbers.
UserClangVersion (String, Int, Maybe Int, Maybe Int)
-- | Unknown version
| UserClangVersionUnknown String
-- | Get the Clang version specified in the @CLANG_VERSION@ macro
--
-- This function returns 'Nothing' when the macro is not set.
getUserClangVersion :: IO (Maybe UserClangVersion)
getUserClangVersion =
get_user_clang_version >>= C.peekCString >>= return . \case
"CLANG_VERSION_NOT_SET" -> Nothing
s -> Just $
case sequence (parse s) of
Just [major, minor, patch] ->
UserClangVersion (s, major, Just minor, Just patch)
Just [major, minor] ->
UserClangVersion (s, major, Just minor, Nothing)
Just [major] ->
UserClangVersion (s, major, Nothing, Nothing)
_otherwise ->
UserClangVersionUnknown s
where
parse :: String -> [Maybe Int]
parse s = case span Char.isDigit s of
(l@(_:_), "") -> [readMaybe l]
(l@(_:_), '.':r) -> readMaybe l : parse r
_otherwise -> [Nothing]
-- | Check the compile-time Clang version against the Clang version specified in
-- the @CLANG_VERSION@ macro
--
-- Users may /optionally/ specify a Clang version in the @CLANG_VERSION@ macro.
-- When set, this function is used to check that the compile-time Clang version
-- matches, failing if not. When not set, no check is performed.
--
-- Only the version number is checked. The following checks are supported:
--
-- * @MAJOR@ to just check the major version (example: @21@)
-- * @MAJOR.MINOR@ to check the major and minor versions (example: @21.1@)
-- * @MAJOR.MINOR.PATCH@ to check the major, minor, and patch versions
-- (example: @21.1.8@)
--
-- The primary motivation for this is to distinguish the Clang version in cached
-- builds in the Cabal store. Cabal does not consider system dependencies to
-- determine when a package needs to be rebuilt. This is particularly
-- problematic for @libclang-bindings@ because the LLVM/Clang project has
-- frequent releases, and it is easy to run into a situation where a library
-- cached in the Cabal store no longer works. This can be solved by clearing
-- the Cabal store, but doing so may result in time-consuming recompilation of
-- many other packages. The @CLANG_VERSION@ macro provides a workaround:
-- changing GHC options forces Cabal to rebuild the library.
--
-- The @CLANG_VERSION@ macro is generally set in a @cabal.project.local@ file,
-- by configuring @-optc@ in @ghc-options@. Note that @cc-options@ is /not/
-- sufficient to force Cabal to rebuild the library. Example:
--
-- @
-- package libclang-bindings
-- ghc-options: -optc=-DCLANG_VERSION=21.1
-- @
checkUserClangVersion :: TH.Q [TH.Dec]
checkUserClangVersion = TH.runIO getUserClangVersion >>= ([] <$) . \case
Nothing -> return ()
Just (UserClangVersionUnknown s) ->
fail $ "Unable to parse CLANG_VERSION: " ++ s
Just (UserClangVersion (uVersion, uMajor, mUMinor, mUPatch)) ->
case parseClangVersion clangVersionCompileTime of
ClangVersionUnknown t ->
fail $ "Unable to parse linked libclang version: " ++ Text.unpack t
ClangVersion (major, minor, patch) -> do
let compatible =
uMajor == major
&& maybe True (== minor) mUMinor
&& maybe True (== patch) mUPatch
unless compatible . fail $
"CLANG_VERSION " ++ uVersion ++ " does not match "
++ Text.unpack clangVersionCompileTime