packages feed

hhp-1.0.5: lib/Hhp/CabalApi.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}

module Hhp.CabalApi (
    getCompilerOptions,
    parseCabalFile,
    cabalAllBuildInfo,
    cabalAllBuildInfo',
    cabalDependPackages,
    cabalSourceDirs,
    cabalAllTargets,
    getHsSourceDir,
) where

import Distribution.Compiler (AbiTag (NoAbiTag), unknownCompilerInfo)
import Distribution.ModuleName (ModuleName, toFilePath)
import Distribution.Package (Dependency (Dependency))
import qualified Distribution.Package as C
import Distribution.PackageDescription (
    BuildInfo,
    Executable (..),
    PackageDescription,
    TestSuite,
    TestSuiteInterface (..),
 )
import qualified Distribution.PackageDescription as P
import Distribution.PackageDescription.Configuration (finalizePD)
import Distribution.Simple.Compiler (CompilerFlavor (..), CompilerId (..))
import Distribution.Simple.Program (ghcProgram)
import Distribution.Simple.Program.Types (programFindVersion, programName)
import Distribution.System (buildPlatform)
import Distribution.Text (display)
import Distribution.Verbosity (silent)
import Distribution.Version (Version)
#if MIN_VERSION_Cabal(3,8,0)
import Distribution.Simple.PackageDescription (readGenericPackageDescription)
#else
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
#endif
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
#if MIN_VERSION_Cabal(3,14,0)
import qualified Distribution.Utils.Path as Path
#endif

import GHC.Utils.Monad (liftIO)

import Control.Exception (throwIO)
import Control.Monad (filterM)
import Data.List (isPrefixOf)
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe, maybeToList)
import Data.Set (fromList, toList)
import System.Directory (doesFileExist)
import System.Environment (lookupEnv)
import System.FilePath (dropExtension, takeDirectory, takeFileName, (</>))

import Hhp.GhcPkg
import Hhp.Types

----------------------------------------------------------------

-- | Getting necessary 'CompilerOptions' from three information sources.
getCompilerOptions
    :: [GHCOption]
    -> Cradle
    -> PackageDescription
    -> Maybe FilePath
    -> IO CompilerOptions
getCompilerOptions ghcopts cradle pkgDesc hsFile = do
    gopts <-
        getGHCOptions ghcopts cradle rdir $ unsafeHead "getCompilerOptions" buildInfos
    dbPkgs <- ghcPkgListEx (cradlePkgDbStack cradle)
    let compOpt = CompilerOptions gopts idirs (depPkgs dbPkgs)
    return compOpt
  where
    wdir = cradleCurrentDir cradle
    rdir = cradleRootDir cradle
    cfile = fromMaybe "error getCompilerOptions" $ cradleCabalFile cradle
    thisPkg = dropExtension $ takeFileName cfile
    buildInfos = cabalAllBuildInfo cradle pkgDesc hsFile
    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 <- readPackageDescription 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" ++) $ extLibDirs 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
    :: Cradle -> PackageDescription -> Maybe FilePath -> [BuildInfo]
cabalAllBuildInfo cradle pd mHsFile = libBI ++ subBI ++ addBI
  where
    libBI = map P.libBuildInfo $ maybeToList $ P.library pd
    subBI = map P.libBuildInfo $ P.subLibraries pd
    addBI = fst $ cabalExtraBuildInfo (Just cradle) pd mHsFile

-- for testing
cabalAllBuildInfo'
    :: PackageDescription -> [BuildInfo]
cabalAllBuildInfo' pd = libBI ++ subBI ++ addBI
  where
    libBI = map P.libBuildInfo $ maybeToList $ P.library pd
    subBI = map P.libBuildInfo $ P.subLibraries pd
    addBI = fst $ cabalExtraBuildInfo Nothing pd Nothing

getHsSourceDir
    :: Cradle -> PackageDescription -> Maybe FilePath -> Maybe FilePath
getHsSourceDir cradle pd mHsFile =
    snd $ cabalExtraBuildInfo (Just cradle) pd mHsFile

cabalExtraBuildInfo
    :: Maybe Cradle
    -> PackageDescription
    -> Maybe FilePath
    -> ([BuildInfo], Maybe FilePath)
cabalExtraBuildInfo mcradle pd mHsFile = (addBI, hsDir addBI)
  where
    execBI = map P.buildInfo $ P.executables pd
    testBI = map P.testBuildInfo $ P.testSuites pd
    benchBI = map P.benchmarkBuildInfo $ P.benchmarks pd
    addBI0 = execBI ++ testBI ++ benchBI
    addBI = case mHsFile of
        Nothing -> addBI0 -- fixme: Is [] more suitable?
        Just hsFile -> case mcradle of
            Nothing -> addBI0 -- fixme: Is [] more suitable?
            Just cradle -> do
                let hsFile' = takeRelativePath cradle hsFile
                 in filter (include hsFile') addBI0
    include hsFile b = any (`match` hsFile) $ map toPath $ P.hsSourceDirs b
    match "." _ = True
    match dir fn = dir `isPrefixOf` fn
    hsDir [] = Nothing
    hsDir (b : _) = toPath <$> listToMaybe (P.hsSourceDirs b)

takeRelativePath :: Cradle -> FilePath -> FilePath
takeRelativePath cradle fn = rp
  where
    root = cradleRootDir cradle
    rp
        | root `isPrefixOf` fn = takeDirectory $ drop (length root + 1) fn
        | otherwise = takeDirectory fn

----------------------------------------------------------------

-- | 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 =
                        [ 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 =
                [ p <//> e
                | p <- P.hsSourceDirs $ P.buildInfo exe
                , e <- [P.modulePath exe]
                ]
        liftIO $ filterM doesFileExist maybeExes

#if MIN_VERSION_Cabal(3,14,0)
(<//>) :: SymbolicPath Path.Pkg (Path.Dir Path.Source)
       -> Path.RelativePath Path.Source Path.File
       -> FilePath
dir <//> file = toPath (dir Path.</> file)
#else
(<//>) :: SymbolicPath from to -> FilePath -> FilePath
dir <//> file = toPath dir </> file
#endif

extLibDirs :: BuildInfo -> [FilePath]
#if MIN_VERSION_Cabal(3,14,0)
extLibDirs = map getSymbolicPath . P.extraLibDirs
#else
extLibDirs = P.extraLibDirs
#endif

#if MIN_VERSION_Cabal(3,6,0)
toPath :: SymbolicPath from to -> FilePath
toPath = getSymbolicPath
#else
toPath :: String -> String
toPath = id
#endif

readPackageDescription :: FilePath -> IO P.GenericPackageDescription
#if MIN_VERSION_Cabal(3,14,0)
readPackageDescription = readGenericPackageDescription silent Nothing . Path.makeSymbolicPath
#else
readPackageDescription = readGenericPackageDescription silent
#endif