{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TupleSections #-} -- | Functions to retrieve and store mapping from modules to their -- targets and extensions. module Importify.Cabal.Target ( -- * Maps from modules paths to cache parts ExtensionsMap , ModulesMap -- * Target types , ModulesBundle (..) , TargetId (..) -- * Utilities to extract targets , extractTargetBuildInfo , extractTargetsMap , packageExtensions , packageTargets , targetIdDir ) where import Universum hiding (fromString) import Data.Aeson (FromJSON (parseJSON), FromJSONKey (fromJSONKey), FromJSONKeyFunction (FromJSONKeyTextParser), ToJSON (toJSON), ToJSONKey (toJSONKey), Value (String), object, withObject, withText, (.:), (.=)) import Data.Aeson.Types (Parser, toJSONKeyText) import Data.Hashable (Hashable) import Distribution.ModuleName (ModuleName) import Distribution.PackageDescription (Benchmark (..), BenchmarkInterface (..), BuildInfo (..), CondTree, Executable (..), GenericPackageDescription (..), Library (..), TestSuite (..), TestSuiteInterface (..), condTreeData) #if MIN_VERSION_Cabal(2,0,0) import Distribution.Types.UnqualComponentName (UnqualComponentName, unUnqualComponentName) #endif import Language.Haskell.Exts (prettyExtension) import Path (Abs, Dir, File, Path, fromAbsFile) import Importify.Cabal.Extension (buildInfoExtensions) import Importify.Cabal.Module (modulePaths) import qualified Data.HashMap.Strict as HM import qualified Data.Text as T (split) -- | Mapping from module path to its package and module name. type ModulesMap = HashMap FilePath ModulesBundle -- cached globally type TargetsMap = HashMap FilePath TargetId -- not cached type ExtensionsMap = HashMap TargetId [String] -- cached per project package data TargetId = LibraryId | ExecutableId !Text | TestSuiteId !Text | BenchmarkId !Text deriving (Show, Eq, Generic) instance Hashable TargetId instance ToJSON TargetId where toJSON = String . targetIdDir instance ToJSONKey TargetId where toJSONKey = toJSONKeyText targetIdDir -- | Directory name for corresponding target. targetIdDir :: TargetId -> Text targetIdDir LibraryId = "library" targetIdDir (ExecutableId exeName) = "executable@" <> exeName targetIdDir (TestSuiteId testName) = "test-suite@" <> testName targetIdDir (BenchmarkId benchName) = "benchmark@" <> benchName instance FromJSON TargetId where parseJSON = withText "targetId" targetIdParser instance FromJSONKey TargetId where fromJSONKey = FromJSONKeyTextParser targetIdParser targetIdParser :: Text -> Parser TargetId targetIdParser targetText = do let targetName = T.split (== '@') targetText case targetName of ["library"] -> pure LibraryId ["executable", exeName] -> pure $ ExecutableId exeName ["test-suite", testName] -> pure $ TestSuiteId testName ["benchmark", benchName] -> pure $ BenchmarkId benchName _ -> fail $ "Unexpected target: " ++ toString targetText -- | All data for given module. This is needed to locate all required -- information about module by its path. data ModulesBundle = ModulesBundle { mbPackage :: !Text -- ^ Module package, like @importify-1.0@ , mbModule :: !String -- ^ Full module name, like @Importify.Main@ , mbTarget :: !TargetId -- ^ Target of module } deriving (Show, Eq) instance ToJSON ModulesBundle where toJSON ModulesBundle{..} = object [ "package" .= mbPackage , "module" .= mbModule , "target" .= mbTarget ] instance FromJSON ModulesBundle where parseJSON = withObject "ModulesBundle" $ \obj -> do mbPackage <- obj .: "package" mbModule <- obj .: "module" mbTarget <- obj .: "target" pure ModulesBundle{..} -- | Extract every 'TargetId' for given project description. packageTargets :: GenericPackageDescription -> [TargetId] packageTargets GenericPackageDescription{..} = concat [ maybe [] (const [LibraryId]) condLibrary , targetMap ExecutableId condExecutables , targetMap TestSuiteId condTestSuites , targetMap BenchmarkId condBenchmarks ] where #if MIN_VERSION_Cabal(2,0,0) targetMap tid = map (tid . toText . unUnqualComponentName . fst) #else targetMap tid = map (tid . toText . fst) #endif -- | Extracts 'BuildInfo' for given 'TargetId'. extractTargetBuildInfo :: TargetId -> GenericPackageDescription -> Maybe BuildInfo extractTargetBuildInfo LibraryId = fmap (libBuildInfo . condTreeData) . condLibrary extractTargetBuildInfo (ExecutableId name) = findTargetBuildInfo buildInfo name . condExecutables extractTargetBuildInfo (TestSuiteId name) = findTargetBuildInfo testBuildInfo name . condTestSuites extractTargetBuildInfo (BenchmarkId name) = findTargetBuildInfo benchmarkBuildInfo name . condBenchmarks #if MIN_VERSION_Cabal(2,0,0) findTargetBuildInfo :: (target -> info) -> Text -> [(UnqualComponentName, CondTree v c target)] -> Maybe info findTargetBuildInfo toInfo name = fmap (toInfo . condTreeData . snd) . find ((== name) . toText . unUnqualComponentName . fst) #else findTargetBuildInfo :: (target -> info) -> Text -> [(String, CondTree v c target)] -> Maybe info findTargetBuildInfo toInfo name = fmap (toInfo . condTreeData . snd) . find ((== name) . toText . fst) #endif -- | Extracts mapping from each package target to its extensions enabled by default. packageExtensions :: [TargetId] -> GenericPackageDescription -> ExtensionsMap packageExtensions targetIds desc = mconcat $ mapMaybe targetToExtensions targetIds where targetToExtensions :: TargetId -> Maybe ExtensionsMap targetToExtensions targetId = toMap targetId <$> extractTargetBuildInfo targetId desc toMap :: TargetId -> BuildInfo -> ExtensionsMap toMap targetId info = one (targetId, map prettyExtension $ buildInfoExtensions info) -- | This function extracts 'ModulesMap' from given package by given -- full path to project root directory. extractTargetsMap :: Path Abs Dir -> GenericPackageDescription -> IO TargetsMap extractTargetsMap projectPath GenericPackageDescription{..} = do libTM <- libMap exeTMs <- exeMaps testTMs <- testMaps benchTMs <- benchMaps return $ HM.unions $ libTM : exeTMs ++ testTMs ++ benchTMs where projectPaths :: BuildInfo -> Either [ModuleName] FilePath -> IO [Path Abs File] projectPaths = modulePaths projectPath libPaths :: Library -> IO [Path Abs File] libPaths Library{..} = projectPaths libBuildInfo (Left exposedModules) exePaths :: Executable -> IO [Path Abs File] exePaths Executable{..} = projectPaths buildInfo (Right modulePath) testPaths :: TestSuite -> IO [Path Abs File] testPaths TestSuite{..} = projectPaths testBuildInfo $ case testInterface of TestSuiteExeV10 _ path -> Right path TestSuiteLibV09 _ name -> Left [name] TestSuiteUnsupported _ -> Left [] benchPaths :: Benchmark -> IO [Path Abs File] benchPaths Benchmark{..} = projectPaths benchmarkBuildInfo $ case benchmarkInterface of BenchmarkExeV10 _ path -> Right path BenchmarkUnsupported _ -> Left [] libMap :: IO TargetsMap libMap = maybe mempty ( collectTargetsMap libPaths LibraryId . condTreeData) condLibrary exeMaps :: IO [TargetsMap] exeMaps = collectTargetsListMaps condExecutables ExecutableId (collectTargetsMap exePaths) testMaps :: IO [TargetsMap] testMaps = collectTargetsListMaps condTestSuites TestSuiteId (collectTargetsMap testPaths) benchMaps :: IO [TargetsMap] benchMaps = collectTargetsListMaps condBenchmarks BenchmarkId (collectTargetsMap benchPaths) -- | Generalized 'TargetsMap' collector for executables, testsuites and -- benchmakrs of package. #if MIN_VERSION_Cabal(2,0,0) collectTargetsListMaps :: [(UnqualComponentName, CondTree v c target)] -> (Text -> TargetId) -> (TargetId -> target -> IO TargetsMap) -> IO [TargetsMap] collectTargetsListMaps treeList idConstructor mapBundler = forM treeList $ \(name, condTree) -> mapBundler (idConstructor $ toText $ unUnqualComponentName name) (condTreeData condTree) #else collectTargetsListMaps :: [(String, CondTree v c target)] -> (Text -> TargetId) -> (TargetId -> target -> IO TargetsMap) -> IO [TargetsMap] collectTargetsListMaps treeList idConstructor mapBundler = forM treeList $ \(name, condTree) -> mapBundler (idConstructor $ toText name) $ condTreeData condTree #endif collectTargetsMap :: (target -> IO [Path Abs File]) -> TargetId -> target -> IO TargetsMap collectTargetsMap modulePathsExtractor targetId target = do pathsToModules <- modulePathsExtractor target return $ constructModulesMap (map fromAbsFile pathsToModules) where constructModulesMap :: [FilePath] -> TargetsMap constructModulesMap = HM.fromList . map (, targetId)