hoogle-5.0.19.0: src/Input/Cabal.hs
{-# LANGUAGE ViewPatterns, PatternGuards, TupleSections, RecordWildCards, ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wall -Wno-name-shadowing #-}
-- | Module for reading Cabal files.
module Input.Cabal(
PkgName, Package(..),
parseCabalTarball, readGhcPkg,
packagePopularity, readCabal
) where
import Input.Settings
import Data.List.Extra
import System.FilePath
import Control.DeepSeq
import Control.Exception.Extra
import Control.Monad
import System.IO.Extra
import General.Str
import System.Exit
import qualified System.Process.ByteString as BS
import qualified Data.ByteString.UTF8 as UTF8
import System.Directory
import Data.Maybe
import Data.Tuple.Extra
import qualified Data.Map.Strict as Map
import General.Util
import Data.Semigroup
import Control.Applicative
import Prelude
import Distribution.Compat.Lens (toListOf)
import qualified Distribution.PackageDescription as PD
import qualified Distribution.PackageDescription.Configuration as PD
import qualified Distribution.PackageDescription.Parsec as PD
import qualified Distribution.Pretty
import qualified Distribution.Types.BuildInfo.Lens as Lens
import Distribution.Types.PackageName (mkPackageName, unPackageName)
import Distribution.Types.Version (versionNumbers)
import Distribution.Utils.ShortText (fromShortText)
import Hackage.RevDeps (lastVersionsOfPackages)
import qualified Distribution.SPDX as SPDX
---------------------------------------------------------------------
-- DATA TYPE
-- | A representation of a Cabal package.
data Package = Package
{packageTags :: ![(Str, Str)] -- ^ The Tag information, e.g. (category,Development) (author,Neil Mitchell).
,packageLibrary :: !Bool -- ^ True if the package provides a library (False if it is only an executable with no API)
,packageSynopsis :: !Str -- ^ The synposis, grabbed from the top section.
,packageVersion :: !Str -- ^ The version, grabbed from the top section.
,packageDepends :: ![PkgName] -- ^ The list of packages that this package directly depends on.
,packageDocs :: !(Maybe FilePath) -- ^ Directory where the documentation is located
} deriving Show
instance Semigroup Package where
Package x1 x2 x3 x4 x5 x6 <> Package y1 y2 y3 y4 y5 y6 =
Package (x1++y1) (x2||y2) (one x3 y3) (one x4 y4) (nubOrd $ x5 ++ y5) (x6 `mplus` y6)
where one a b = if strNull a then b else a
instance Monoid Package where
mempty = Package [] True mempty mempty [] Nothing
mappend = (<>)
instance NFData Package where
rnf (Package a b c d e f) = rnf (a,b,c,d,e,f)
---------------------------------------------------------------------
-- POPULARITY
-- | Given a set of packages, return the popularity of each package, along with any warnings
-- about packages imported but not found.
packagePopularity :: Map.Map PkgName Package -> ([String], Map.Map PkgName Int)
packagePopularity cbl = mp `seq` (errs, mp)
where
mp = Map.map length good
errs = [ unPackageName user ++ ".cabal: Import of non-existant package " ++ unPackageName name ++
(if null rest then "" else ", also imported by " ++ show (length rest) ++ " others")
| (name, user:rest) <- Map.toList bad]
(good, bad) = Map.partitionWithKey (\k _ -> k `Map.member` cbl) $
Map.fromListWith (++) [(b,[a]) | (a,bs) <- Map.toList cbl, b <- packageDepends bs]
---------------------------------------------------------------------
-- READERS
-- | Run 'ghc-pkg' and get a list of packages which are installed.
readGhcPkg :: Settings -> IO (Map.Map PkgName Package)
readGhcPkg settings = do
topdir <- findExecutable "ghc-pkg"
(exit, stdout, stderr) <-
-- From GHC 9.0.1, the `haddock-html` field in `*.conf` files for GHC boot
-- libraries has used `${pkgroot}`, which can be expanded in the output.
-- On Windows, the `haddock-html` field in `*.conf` files for GHC boot
-- libraries for GHC >= 9.0 && < 9.10 contain errors. For example, this may
-- be specified:
--
-- haddock-html: ${pkgroot}/../../doc/html/libraries/base-4.18.0.0
--
-- when the correct specification would be:
--
-- haddock-html: ${pkgroot}/../doc/html/libraries/base-4.18.0.0
--
-- However haddock does not seek to correct that. It assumes that users will
-- correct manually the affected `*.conf` files.
-- important to use BS process reading so it's in Binary format, see #194
BS.readProcessWithExitCode "ghc-pkg" ["dump", "--expand-pkgroot"] mempty
when (exit /= ExitSuccess) $
errorIO $ "Error when reading from ghc-pkg, " ++ show exit ++ "\n" ++ UTF8.toString stderr
let g (stripPrefix "$topdir" -> Just x) | Just t <- topdir = takeDirectory t ++ x
-- ^ Backwards compatibility with GHC < 9.0
g x = x
let fixer p = p{packageLibrary = True, packageDocs = g <$> packageDocs p}
let f ((stripPrefix "name: " -> Just x):xs) = Just (mkPackageName $ trimStart x, fixer $ readCabal settings $ bstrPack $ unlines xs)
f _ = Nothing
pure $ Map.fromList $ mapMaybe f $ splitOn ["---"] $ lines $ filter (/= '\r') $ UTF8.toString stdout
-- | Given a tarball of Cabal files, parse the latest version of each package.
parseCabalTarball :: Settings -> FilePath -> IO (Map.Map PkgName Package)
parseCabalTarball settings tarfile = do
lastVersions <- lastVersionsOfPackages (const True) tarfile Nothing
pure $ Map.map (readCabal settings) lastVersions
---------------------------------------------------------------------
-- PARSERS
readCabal :: Settings -> BStr -> Package
readCabal settings src = case PD.parseGenericPackageDescriptionMaybe src of
Nothing -> Package
{ packageTags = []
, packageLibrary = False
, packageSynopsis = mempty
, packageVersion = strPack "0.0"
, packageDepends = []
, packageDocs = Nothing
}
Just gpd -> readCabal' settings gpd
readCabal' :: Settings -> PD.GenericPackageDescription -> Package
readCabal' Settings{..} gpd = Package{..}
where
pd = PD.flattenPackageDescription gpd
pkgId = PD.package pd
packageDepends = nubOrd $ foldMap (map (\(PD.Dependency pkg _ _) -> pkg) . PD.targetBuildDepends) $ toListOf Lens.traverseBuildInfos gpd
packageVersion = strPack $ intercalate "." $ map show $ versionNumbers $ PD.pkgVersion pkgId
packageSynopsis = strPack $ fromShortText $ PD.synopsis pd
packageLibrary = PD.hasPublicLib pd
packageDocs = Nothing
unpackLicenseExpression (SPDX.EOr x y) = unpackLicenseExpression x ++ unpackLicenseExpression y
unpackLicenseExpression x = [x]
packageLicenses = case PD.license pd of
SPDX.NONE -> []
SPDX.License licExpr -> map (show . Distribution.Pretty.pretty) $
unpackLicenseExpression licExpr
packageCategories =
filter (not . null) $ split (`elem` " ,") $
fromShortText $ PD.category pd
packageAuthor = fromShortText $ PD.author pd
packageMaintainer = fromShortText $ PD.maintainer pd
packageTags = map (both strPack) $ nubOrd $ concat
[ map ("license",) packageLicenses
, map ("category",) packageCategories
, map ("author",) (concatMap cleanup [packageAuthor, packageMaintainer])
]
-- split on things like "," "&" "and", then throw away email addresses, replace spaces with "-" and rename
cleanup =
filter (/= "") .
map (renameTag . intercalate "-" . filter ('@' `notElem`) . words . takeWhile (`notElem` "<(")) .
concatMap (map unwords . split (== "and") . words) . split (`elem` ",&")