{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Hhp.CabalApi ( getCompilerOptions , parseCabalFile , cabalAllBuildInfo , cabalDependPackages , cabalSourceDirs , cabalAllTargets ) where import Distribution.Compiler (unknownCompilerInfo, AbiTag(NoAbiTag)) import Distribution.ModuleName (ModuleName,toFilePath) import Distribution.Package (Dependency(Dependency)) import qualified Distribution.Package as C import Distribution.PackageDescription (PackageDescription, BuildInfo, TestSuite, TestSuiteInterface(..), Executable) import qualified Distribution.PackageDescription as P import Distribution.Simple.Compiler (CompilerId(..), CompilerFlavor(..)) import Distribution.Simple.Program (ghcProgram) import Distribution.Simple.Program.Types (programName, programFindVersion) import Distribution.System (buildPlatform) import Distribution.Text (display) import Distribution.Verbosity (silent) import Distribution.Version (Version) import Distribution.PackageDescription.Configuration (finalizePD) import Distribution.PackageDescription.Parsec (readGenericPackageDescription) import Distribution.Types.ComponentRequestedSpec (defaultComponentRequestedSpec) import Distribution.Types.Flag (mkFlagAssignment, mkFlagName) import Distribution.Types.PackageName (unPackageName) #if MIN_VERSION_Cabal(3,6,0) import Distribution.Utils.Path (getSymbolicPath, SymbolicPath) #endif import GHC.Utils.Monad (liftIO) import Control.Exception (throwIO) import Control.Monad (filterM) import Data.Maybe (maybeToList, mapMaybe) import Data.Set (fromList, toList) import System.Directory (doesFileExist) import System.Environment (lookupEnv) import System.FilePath (dropExtension, takeFileName, ()) import Hhp.Types import Hhp.GhcPkg ---------------------------------------------------------------- -- | Getting necessary 'CompilerOptions' from three information sources. getCompilerOptions :: [GHCOption] -> Cradle -> PackageDescription -> IO CompilerOptions getCompilerOptions ghcopts cradle pkgDesc = do gopts <- getGHCOptions ghcopts cradle rdir $ head buildInfos dbPkgs <- ghcPkgListEx (cradlePkgDbStack cradle) return $ CompilerOptions gopts idirs (depPkgs dbPkgs) where wdir = cradleCurrentDir cradle rdir = cradleRootDir cradle Just cfile = cradleCabalFile cradle thisPkg = dropExtension $ takeFileName cfile buildInfos = cabalAllBuildInfo pkgDesc idirs = includeDirectories rdir wdir $ cabalSourceDirs buildInfos depPkgs ps = attachPackageIds ps $ removeThem (problematicPackages ++ [thisPkg]) $ cabalDependPackages buildInfos ---------------------------------------------------------------- -- Dependent packages removeThem :: [PackageBaseName] -> [PackageBaseName] -> [PackageBaseName] removeThem badpkgs = filter (`notElem` badpkgs) problematicPackages :: [PackageBaseName] problematicPackages = [ "base-compat" -- providing "Prelude" ] attachPackageIds :: [Package] -> [PackageBaseName] -> [Package] attachPackageIds pkgs = mapMaybe (`lookup3` pkgs) lookup3 :: Eq a => a -> [(a,b,c)] -> Maybe (a,b,c) lookup3 _ [] = Nothing lookup3 k (t@(a,_,_):ls) | k == a = Just t | otherwise = lookup3 k ls ---------------------------------------------------------------- -- Include directories for modules cabalBuildDirs :: [FilePath] cabalBuildDirs = ["dist/build", "dist/build/autogen"] includeDirectories :: FilePath -> FilePath -> [FilePath] -> [FilePath] includeDirectories cdir wdir dirs = uniqueAndSort (extdirs ++ [cdir,wdir]) where extdirs = map expand $ dirs ++ cabalBuildDirs expand "." = cdir expand subdir = cdir subdir ---------------------------------------------------------------- -- | Parsing a cabal file and returns 'PackageDescription'. -- 'IOException' is thrown if parsing fails. parseCabalFile :: FilePath -> IO PackageDescription parseCabalFile file = do cid <- getGHCId let cid' = unknownCompilerInfo cid NoAbiTag epgd <- readGenericPackageDescription silent file flags <- getFlags case toPkgDesc cid' flags epgd of Left deps -> throwIO $ userError $ show deps ++ " are not installed" Right (pd,_) -> if nullPkg pd then throwIO $ userError $ file ++ " is broken" else return pd where envFlags = do let parseF [] = [] parseF ccs@(c:cs) | c == '-' = [(mkFlagName cs, False)] | otherwise = [(mkFlagName ccs, True)] maybe [] (concatMap parseF . words) `fmap` lookupEnv "HHP_CABAL_FLAGS" getFlags = mkFlagAssignment <$> envFlags nullPkg pd = unPackageName (C.pkgName (P.package pd)) == "" toPkgDesc cid flags = finalizePD flags defaultComponentRequestedSpec (const True) buildPlatform cid [] ---------------------------------------------------------------- getGHCOptions :: [GHCOption] -> Cradle -> FilePath -> BuildInfo -> IO [GHCOption] getGHCOptions ghcopts cradle rdir binfo = do cabalCpp <- cabalCppOptions rdir let cpps = map ("-optP" ++) $ P.cppOptions binfo ++ cabalCpp return $ ghcopts ++ pkgDb ++ exts ++ [lang] ++ libs ++ libDirs ++ cpps where pkgDb = ghcDbStackOpts $ cradlePkgDbStack cradle lang = maybe "-XHaskell98" (("-X" ++) . display) $ P.defaultLanguage binfo libDirs = map ("-L" ++) $ P.extraLibDirs binfo exts = map (("-X" ++) . display) $ P.usedExtensions binfo libs = map ("-l" ++) $ P.extraLibs binfo cabalCppOptions :: FilePath -> IO [String] cabalCppOptions dir = do exist <- doesFileExist cabalMacro return $ if exist then ["-include", cabalMacro] else [] where cabalMacro = dir "dist/build/autogen/cabal_macros.h" ---------------------------------------------------------------- -- | Extracting all 'BuildInfo' for libraries, executables, and tests. cabalAllBuildInfo :: PackageDescription -> [BuildInfo] cabalAllBuildInfo pd = libBI ++ subBI ++ execBI ++ testBI ++ benchBI where libBI = map P.libBuildInfo $ maybeToList $ P.library pd subBI = map P.libBuildInfo $ P.subLibraries pd execBI = map P.buildInfo $ P.executables pd testBI = map P.testBuildInfo $ P.testSuites pd benchBI = map P.benchmarkBuildInfo $ P.benchmarks pd ---------------------------------------------------------------- -- | Extracting package names of dependency. cabalDependPackages :: [BuildInfo] -> [PackageBaseName] cabalDependPackages bis = uniqueAndSort pkgs where pkgs = map getDependencyPackageName $ concatMap P.targetBuildDepends bis getDependencyPackageName (Dependency pkg _ _) = unPackageName pkg ---------------------------------------------------------------- -- | Extracting include directories for modules. cabalSourceDirs :: [BuildInfo] -> [IncludeDir] cabalSourceDirs bis = uniqueAndSort $ concatMap (map toPath . P.hsSourceDirs) bis ---------------------------------------------------------------- uniqueAndSort :: [String] -> [String] uniqueAndSort = toList . fromList ---------------------------------------------------------------- getGHCId :: IO CompilerId getGHCId = CompilerId GHC <$> getGHC getGHC :: IO Version getGHC = do mv <- programFindVersion ghcProgram silent (programName ghcProgram) case mv of Nothing -> throwIO $ userError "ghc not found" Just v -> return v ---------------------------------------------------------------- -- | Extracting all 'Module' 'FilePath's for libraries, executables, -- tests and benchmarks. cabalAllTargets :: PackageDescription -> IO ([String],[String],[String],[String]) cabalAllTargets pd = do exeTargets <- mapM getExecutableTarget $ P.executables pd testTargets <- mapM getTestTarget $ P.testSuites pd return (libTargets,concat exeTargets,concat testTargets,benchTargets) where lib = maybe [] P.explicitLibModules $ P.library pd libTargets = map toModuleString lib benchTargets = map toModuleString $ concatMap P.benchmarkModules $ P.benchmarks pd toModuleString :: ModuleName -> String toModuleString mn = fromFilePath $ toFilePath mn fromFilePath :: FilePath -> String fromFilePath fp = map (\c -> if c=='/' then '.' else c) fp getTestTarget :: TestSuite -> IO [String] getTestTarget ts = case P.testInterface ts of (TestSuiteExeV10 _ filePath) -> do let maybeTests = [toPath p e | p <- P.hsSourceDirs $ P.testBuildInfo ts, e <- [filePath]] liftIO $ filterM doesFileExist maybeTests (TestSuiteLibV09 _ moduleName) -> return [toModuleString moduleName] (TestSuiteUnsupported _) -> return [] getExecutableTarget :: Executable -> IO [String] getExecutableTarget exe = do let maybeExes = [toPath p e | p <- P.hsSourceDirs $ P.buildInfo exe, e <- [P.modulePath exe]] liftIO $ filterM doesFileExist maybeExes #if MIN_VERSION_Cabal(3,6,0) toPath :: SymbolicPath from to -> FilePath toPath = getSymbolicPath #else toPath :: String -> String toPath = id #endif