prune-juice-0.6: src/Data/Prune/Cabal.hs
module Data.Prune.Cabal where
import Prelude
import Cabal.Project (prjPackages, readProject)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Logger (MonadLogger, logDebug)
import Data.Maybe (maybeToList)
import Data.Prune.File (listFilesRecursive)
import Data.Set (Set)
import Data.Text (Text, pack)
import Data.Traversable (for)
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
import Distribution.Types.Benchmark (Benchmark)
import Distribution.Types.BuildInfo (BuildInfo)
import Distribution.Types.CondTree (CondTree)
import Distribution.Types.Dependency (Dependency)
import Distribution.Types.Executable (Executable)
import Distribution.Types.Library (Library)
import Distribution.Types.TestSuite (TestSuite)
import System.Directory (listDirectory)
import System.FilePath.Posix ((</>), isExtensionOf, takeDirectory, takeFileName)
import qualified Data.Set as Set
import qualified Distribution.Types.Benchmark as Benchmark
import qualified Distribution.Types.BenchmarkInterface as BenchmarkInterface
import qualified Distribution.Types.BuildInfo as BuildInfo
import qualified Distribution.Types.CondTree as CondTree
import qualified Distribution.Types.Dependency as Dependency
import qualified Distribution.Types.Executable as Executable
import qualified Distribution.Types.GenericPackageDescription as GenericPackageDescription
import qualified Distribution.Types.Library as Library
import qualified Distribution.Types.PackageDescription as PackageDescription
import qualified Distribution.Types.PackageId as PackageId
import qualified Distribution.Types.PackageName as PackageName
import qualified Distribution.Types.TestSuite as TestSuite
import qualified Distribution.Types.TestSuiteInterface as TestSuiteInterface
import qualified Distribution.Types.UnqualComponentName as UnqualComponentName
import qualified Distribution.Verbosity as Verbosity
import qualified Data.Prune.Types as T
-- |Get the dependencies for a thing to compile.
getDependencyNames :: Set T.DependencyName -> [Dependency] -> Set T.DependencyName
getDependencyNames ignores = flip Set.difference ignores . Set.fromList . map (T.DependencyName . pack. PackageName.unPackageName . Dependency.depPkgName)
-- |Get the Haskell source files to compile.
getSourceFiles :: FilePath -> Maybe FilePath -> BuildInfo -> IO (Set FilePath)
getSourceFiles fp mainMay buildInfo = do
let hsSourceDirs = BuildInfo.hsSourceDirs buildInfo
allFiles <- case null hsSourceDirs of
True -> Set.filter (flip elem (takeFileName <$> maybeToList mainMay) . takeFileName) <$> listFilesRecursive fp
False -> fmap mconcat . for hsSourceDirs $ \dir -> listFilesRecursive $ fp </> dir
pure $ Set.filter (\fp2 -> any ($ fp2) [isExtensionOf "hs", isExtensionOf "lhs", isExtensionOf "hs-boot"]) allFiles
-- |Parse a library to compile.
getLibraryCompilable :: FilePath -> Set T.DependencyName -> Text -> CondTree a [Dependency] Library -> IO T.Compilable
getLibraryCompilable fp ignores name tree = do
let compilableName = T.CompilableName name
sourceFiles <- getSourceFiles fp Nothing . Library.libBuildInfo . CondTree.condTreeData $ tree
pure $ T.Compilable compilableName T.CompilableTypeLibrary (getDependencyNames ignores $ CondTree.condTreeConstraints tree) sourceFiles
-- |Parse an executable to compile.
getExecutableCompilable :: FilePath -> Set T.DependencyName -> Text -> CondTree a [Dependency] Executable -> IO T.Compilable
getExecutableCompilable fp ignores name tree = do
let compilableName = T.CompilableName name
mainMay = Just . Executable.modulePath . CondTree.condTreeData $ tree
sourceFiles <- getSourceFiles fp mainMay . Executable.buildInfo . CondTree.condTreeData $ tree
pure $ T.Compilable compilableName T.CompilableTypeExecutable (getDependencyNames ignores $ CondTree.condTreeConstraints tree) sourceFiles
-- |Parse a test to compile.
getTestCompilable :: FilePath -> Set T.DependencyName -> Text -> CondTree a [Dependency] TestSuite -> IO T.Compilable
getTestCompilable fp ignores name tree = do
let compilableName = T.CompilableName name
mainMay = case TestSuite.testInterface $ CondTree.condTreeData tree of
TestSuiteInterface.TestSuiteExeV10 _ exe -> Just exe
TestSuiteInterface.TestSuiteLibV09 _ _ -> Nothing
TestSuiteInterface.TestSuiteUnsupported _ -> Nothing
sourceFiles <- getSourceFiles fp mainMay . TestSuite.testBuildInfo . CondTree.condTreeData $ tree
pure $ T.Compilable compilableName T.CompilableTypeTest (getDependencyNames ignores $ CondTree.condTreeConstraints tree) sourceFiles
-- |Parse a benchmark to compile.
getBenchmarkCompilable :: FilePath -> Set T.DependencyName -> Text -> CondTree a [Dependency] Benchmark -> IO T.Compilable
getBenchmarkCompilable fp ignores name tree = do
let compilableName = T.CompilableName name
mainMay = case Benchmark.benchmarkInterface $ CondTree.condTreeData tree of
BenchmarkInterface.BenchmarkExeV10 _ exe -> Just exe
BenchmarkInterface.BenchmarkUnsupported _ -> Nothing
sourceFiles <- getSourceFiles fp mainMay . Benchmark.benchmarkBuildInfo . CondTree.condTreeData $ tree
pure $ T.Compilable compilableName T.CompilableTypeBenchmark (getDependencyNames ignores $ CondTree.condTreeConstraints tree) sourceFiles
-- |Parse a single cabal file.
parseCabalFile :: FilePath -> Set T.DependencyName -> IO T.Package
parseCabalFile fp ignores = do
cabalFile <- maybe (fail $ "No .cabal file found in " <> fp) pure . T.headMay . filter (isExtensionOf "cabal") =<< listDirectory fp
genericPackageDescription <- readGenericPackageDescription Verbosity.silent $ fp </> cabalFile
let baseDependencies = case GenericPackageDescription.condLibrary genericPackageDescription of
Just library -> getDependencyNames ignores $ CondTree.condTreeConstraints library
-- if no library, use set intersection to figure out the common dependencies and use that as the base
Nothing ->
let allCabalDependencies :: [Set T.DependencyName]
allCabalDependencies = fmap (getDependencyNames ignores) . mconcat $
[ CondTree.condTreeConstraints . snd <$> GenericPackageDescription.condSubLibraries genericPackageDescription
, CondTree.condTreeConstraints . snd <$> GenericPackageDescription.condExecutables genericPackageDescription
, CondTree.condTreeConstraints . snd <$> GenericPackageDescription.condTestSuites genericPackageDescription
, CondTree.condTreeConstraints . snd <$> GenericPackageDescription.condBenchmarks genericPackageDescription
]
in case allCabalDependencies of
[] -> mempty
x:xs -> foldr Set.intersection x xs
packageDescription = GenericPackageDescription.packageDescription genericPackageDescription
packageName = pack . PackageName.unPackageName . PackageId.pkgName . PackageDescription.package $ packageDescription
libraries <- traverse (getLibraryCompilable fp (ignores <> baseDependencies) packageName) . maybeToList . GenericPackageDescription.condLibrary $ genericPackageDescription
internalLibraries <- traverse (getLibraryCompilable fp (ignores <> baseDependencies) <$> pack . UnqualComponentName.unUnqualComponentName . fst <*> snd) . GenericPackageDescription.condSubLibraries $ genericPackageDescription
executables <- traverse (getExecutableCompilable fp (ignores <> baseDependencies) <$> pack . UnqualComponentName.unUnqualComponentName . fst <*> snd) . GenericPackageDescription.condExecutables $ genericPackageDescription
tests <- traverse (getTestCompilable fp (ignores <> baseDependencies) <$> pack . UnqualComponentName.unUnqualComponentName . fst <*> snd) . GenericPackageDescription.condTestSuites $ genericPackageDescription
benchmarks <- traverse (getBenchmarkCompilable fp (ignores <> baseDependencies) <$> pack . UnqualComponentName.unUnqualComponentName . fst <*> snd) . GenericPackageDescription.condBenchmarks $ genericPackageDescription
pure T.Package
{ packageName = packageName
, packageBaseDependencies = baseDependencies
, packageCompilables = libraries <> internalLibraries <> executables <> tests <> benchmarks
}
-- |Parse cabal files by file path, filter by explicit package names (if provided), and return the parsed packages.
parseCabalFiles :: (MonadIO m, MonadLogger m) => [FilePath] -> Set T.DependencyName -> [Text] -> m [T.Package]
parseCabalFiles packageDirs ignores packages = do
rawPackages <- liftIO $ traverse (flip parseCabalFile ignores) packageDirs
let toReturn = if null packages then rawPackages else filter (flip elem packages . T.packageName) rawPackages
$logDebug $ "Parsed packages " <> pack (show toReturn)
pure toReturn
findCabalFiles :: FilePath -> IO (T.BuildSystem, [FilePath])
findCabalFiles projectRoot = do
(T.Cabal,) . map takeDirectory . filter (isExtensionOf "cabal") . Set.toList <$> listFilesRecursive projectRoot
-- |Parse cabal.project file by file path.
parseCabalProjectFile :: FilePath -> IO (T.BuildSystem, [FilePath])
parseCabalProjectFile cabalProjectFile = do
project <- readProject cabalProjectFile
pure (T.CabalProject, takeDirectory . fst <$> prjPackages project)