{-# LANGUAGE ScopedTypeVariables #-} ----------------------------------------------------------------------------- ----------------------------------------------------------------------------- -- | -- Module : Distribution.Client.List -- Copyright : (c) David Himmelstrup 2005 -- Duncan Coutts 2008-2011 -- License : BSD-like -- -- Maintainer : cabal-devel@haskell.org -- -- Search for and print information about packages module Distribution.Client.List ( list , info ) where import Distribution.Client.Compat.Prelude import Prelude () import qualified Distribution.InstalledPackageInfo as Installed import Distribution.License (License) import Distribution.ModuleName (ModuleName) import Distribution.Package ( Package (..) , PackageName , UnitId , packageName , packageVersion ) import Distribution.PackageDescription ( PackageFlag (..) , unFlagName ) import qualified Distribution.PackageDescription as Source import Distribution.PackageDescription.Configuration ( flattenPackageDescription ) import Distribution.Types.Dependency import Distribution.Types.UnqualComponentName import Distribution.Simple.Compiler import Distribution.Simple.PackageIndex (InstalledPackageIndex) import qualified Distribution.Simple.PackageIndex as InstalledPackageIndex import Distribution.Simple.Program (ProgramDb) import Distribution.Simple.Setup (fromFlag, fromFlagOrDefault) import Distribution.Simple.Utils ( dieWithException , equating , notice ) import Distribution.Version ( Version , VersionRange , anyVersion , intersectVersionRanges , mkVersion , simplifyVersionRange , versionNumbers , withinRange ) import qualified Distribution.SPDX as SPDX import Distribution.Solver.Types.PackageConstraint import qualified Distribution.Solver.Types.PackageIndex as PackageIndex import Distribution.Solver.Types.SourcePackage import Distribution.Client.FetchUtils ( isFetched ) import Distribution.Client.IndexUtils as IndexUtils ( getInstalledPackages , getSourcePackages ) import Distribution.Client.Setup ( GlobalFlags (..) , InfoFlags (..) , ListFlags (..) , RepoContext (..) ) import Distribution.Client.Targets ( UserTarget , resolveUserTargets ) import Distribution.Client.Types ( PackageSpecifier (..) , SourcePackageDb (..) , UnresolvedSourcePackage ) import Distribution.Client.Utils ( MergeResult (..) , mergeBy ) import Control.Exception ( assert ) import Data.Bits ((.|.)) import Data.List ( maximumBy ) import qualified Data.List as L import Data.List.NonEmpty (groupBy) import qualified Data.Map as Map import Data.Maybe ( fromJust ) import Data.Tree as Tree import System.Directory ( doesDirectoryExist ) import Text.PrettyPrint ( Doc , char , fsep , lineLength , nest , parens , renderStyle , ribbonsPerLine , style , text , vcat , ($+$) ) import qualified Text.PrettyPrint as Disp import Distribution.Client.Errors import Distribution.Utils.ShortText (ShortText) import qualified Distribution.Utils.ShortText as ShortText import qualified Text.Regex.Base as Regex import qualified Text.Regex.Posix.String as Regex -- | Return a list of packages matching given search strings. getPkgList :: Verbosity -> PackageDBStackCWD -> RepoContext -> Maybe (Compiler, ProgramDb) -> ListFlags -> [String] -> IO [PackageDisplayInfo] getPkgList verbosity packageDBs repoCtxt mcompprogdb listFlags pats = do installedPkgIndex <- for mcompprogdb $ \(comp, progdb) -> getInstalledPackages verbosity comp packageDBs progdb sourcePkgDb <- getSourcePackages verbosity repoCtxt regexps <- for pats $ \pat -> do e <- Regex.compile compOption Regex.execBlank pat case e of Right r -> return r Left err -> dieWithException verbosity $ GetPkgList pat err let sourcePkgIndex = packageIndex sourcePkgDb prefs name = fromMaybe anyVersion (Map.lookup name (packagePreferences sourcePkgDb)) pkgsInfoMatching :: [(PackageName, [Installed.InstalledPackageInfo], [UnresolvedSourcePackage])] pkgsInfoMatching = let matchingInstalled = maybe [] (matchingPackages InstalledPackageIndex.searchWithPredicate regexps) installedPkgIndex matchingSource = matchingPackages (\idx n -> concatMap snd (PackageIndex.searchWithPredicate idx n)) regexps sourcePkgIndex in mergePackages matchingInstalled matchingSource pkgsInfo :: [(PackageName, [Installed.InstalledPackageInfo], [UnresolvedSourcePackage])] pkgsInfo -- gather info for all packages | null regexps = mergePackages (maybe [] InstalledPackageIndex.allPackages installedPkgIndex) (PackageIndex.allPackages sourcePkgIndex) -- gather info for packages matching search term | otherwise = pkgsInfoMatching matches :: [PackageDisplayInfo] matches = [ mergePackageInfo pref installedPkgs sourcePkgs selectedPkg False | (pkgname, installedPkgs, sourcePkgs) <- pkgsInfo , not onlyInstalled || not (null installedPkgs) , let pref = prefs pkgname selectedPkg = latestWithPref pref sourcePkgs ] return matches where onlyInstalled = fromFlagOrDefault False (listInstalled listFlags) caseInsensitive = fromFlagOrDefault True (listCaseInsensitive listFlags) compOption | caseInsensitive = Regex.compExtended .|. Regex.compIgnoreCase | otherwise = Regex.compExtended matchingPackages search regexps index = [ pkg | re <- regexps , pkg <- search index (Regex.matchTest re) ] -- | Show information about packages. list :: Verbosity -> PackageDBStackCWD -> RepoContext -> Maybe (Compiler, ProgramDb) -> ListFlags -> [String] -> IO () list verbosity packageDBs repos mcompProgdb listFlags pats = do matches <- getPkgList verbosity packageDBs repos mcompProgdb listFlags pats if simpleOutput then putStr $ unlines [ prettyShow (pkgName pkg) ++ " " ++ prettyShow version | pkg <- matches , version <- if onlyInstalled then installedVersions pkg else nub . sort $ installedVersions pkg ++ sourceVersions pkg ] else -- Note: this only works because for 'list', one cannot currently -- specify any version constraints, so listing all installed -- and source ones works. if null matches then notice verbosity "No matches found." else putStr $ unlines (map showPackageSummaryInfo matches) where onlyInstalled = fromFlag (listInstalled listFlags) simpleOutput = fromFlag (listSimpleOutput listFlags) info :: Verbosity -> PackageDBStackCWD -> RepoContext -> Compiler -> ProgramDb -> GlobalFlags -> InfoFlags -> [UserTarget] -> IO () info verbosity _ _ _ _ _ _ [] = notice verbosity "No packages requested. Nothing to do." info verbosity packageDBs repoCtxt comp progdb _ _listFlags userTargets = do installedPkgIndex <- getInstalledPackages verbosity comp packageDBs progdb sourcePkgDb <- getSourcePackages verbosity repoCtxt let sourcePkgIndex = packageIndex sourcePkgDb prefs name = fromMaybe anyVersion (Map.lookup name (packagePreferences sourcePkgDb)) -- Users may specify names of packages that are only installed, not -- just available source packages, so we must resolve targets using -- the combination of installed and source packages. let sourcePkgs' = PackageIndex.fromList $ map packageId (InstalledPackageIndex.allPackages installedPkgIndex) ++ map packageId (PackageIndex.allPackages sourcePkgIndex) pkgSpecifiers <- resolveUserTargets verbosity repoCtxt sourcePkgs' userTargets pkgsinfo <- sequenceA [ do pkginfo <- either (dieWithException verbosity) return $ gatherPkgInfo prefs installedPkgIndex sourcePkgIndex pkgSpecifier updateFileSystemPackageDetails pkginfo | pkgSpecifier <- pkgSpecifiers ] putStr $ unlines (map showPackageDetailedInfo pkgsinfo) where gatherPkgInfo :: (PackageName -> VersionRange) -> InstalledPackageIndex -> PackageIndex.PackageIndex UnresolvedSourcePackage -> PackageSpecifier UnresolvedSourcePackage -> Either CabalInstallException PackageDisplayInfo gatherPkgInfo prefs installedPkgIndex sourcePkgIndex (NamedPackage name props) | null (selectedInstalledPkgs) && null (selectedSourcePkgs) = Left $ GatherPkgInfo name (simplifyVersionRange verConstraint) | otherwise = Right $ mergePackageInfo pref installedPkgs sourcePkgs selectedSourcePkg' showPkgVersion where (pref, installedPkgs, sourcePkgs) = sourcePkgsInfo prefs name installedPkgIndex sourcePkgIndex selectedInstalledPkgs = InstalledPackageIndex.lookupDependency installedPkgIndex name verConstraint selectedSourcePkgs = PackageIndex.lookupDependency sourcePkgIndex name verConstraint selectedSourcePkg' = latestWithPref pref selectedSourcePkgs -- display a specific package version if the user -- supplied a non-trivial version constraint showPkgVersion = not (null verConstraints) verConstraint = foldr intersectVersionRanges anyVersion verConstraints verConstraints = [vr | PackagePropertyVersion vr <- props] gatherPkgInfo prefs installedPkgIndex sourcePkgIndex (SpecificSourcePackage pkg) = Right $ mergePackageInfo pref installedPkgs sourcePkgs selectedPkg True where name = packageName pkg selectedPkg = Just pkg (pref, installedPkgs, sourcePkgs) = sourcePkgsInfo prefs name installedPkgIndex sourcePkgIndex sourcePkgsInfo :: (PackageName -> VersionRange) -> PackageName -> InstalledPackageIndex -> PackageIndex.PackageIndex UnresolvedSourcePackage -> (VersionRange, [Installed.InstalledPackageInfo], [UnresolvedSourcePackage]) sourcePkgsInfo prefs name installedPkgIndex sourcePkgIndex = (pref, installedPkgs, sourcePkgs) where pref = prefs name installedPkgs = concatMap snd ( InstalledPackageIndex.lookupPackageName installedPkgIndex name ) sourcePkgs = PackageIndex.lookupPackageName sourcePkgIndex name -- | The info that we can display for each package. It is information per -- package name and covers all installed and available versions. data PackageDisplayInfo = PackageDisplayInfo { pkgName :: PackageName , selectedVersion :: Maybe Version , selectedSourcePkg :: Maybe UnresolvedSourcePackage , installedVersions :: [Version] , sourceVersions :: [Version] , preferredVersions :: VersionRange , homepage :: ShortText , bugReports :: ShortText , sourceRepo :: String -- TODO , synopsis :: ShortText , description :: ShortText , category :: ShortText , license :: Either SPDX.License License , author :: ShortText , maintainer :: ShortText , dependencies :: [ExtDependency] , flags :: [PackageFlag] , hasLib :: Bool , hasExe :: Bool , executables :: [UnqualComponentName] , modules :: [ModuleName] , haddockHtml :: FilePath , haveTarball :: Bool } -- | Covers source dependencies and installed dependencies in -- one type. data ExtDependency = SourceDependency Dependency | InstalledDependency UnitId showPackageSummaryInfo :: PackageDisplayInfo -> String showPackageSummaryInfo pkginfo = renderStyle (style{lineLength = 80, ribbonsPerLine = 1}) $ char '*' <+> pretty (pkgName pkginfo) $+$ nest 4 ( vcat [ maybeShowST (synopsis pkginfo) "Synopsis:" reflowParagraphs , text "Default available version:" <+> case selectedSourcePkg pkginfo of Nothing -> text "[ Not available from any configured repository ]" Just pkg -> pretty (packageVersion pkg) , text "Installed versions:" <+> case installedVersions pkginfo of [] | hasLib pkginfo -> text "[ Not installed ]" | otherwise -> text "[ Unknown ]" versions -> dispTopVersions 4 (preferredVersions pkginfo) versions , maybeShowST (homepage pkginfo) "Homepage:" text , text "License: " <+> either pretty pretty (license pkginfo) ] ) $+$ text "" where maybeShowST l s f | ShortText.null l = Disp.empty | otherwise = text s <+> f (ShortText.fromShortText l) showPackageDetailedInfo :: PackageDisplayInfo -> String showPackageDetailedInfo pkginfo = renderStyle (style{lineLength = 80, ribbonsPerLine = 1}) $ char '*' <+> pretty (pkgName pkginfo) <<>> maybe Disp.empty (\v -> char '-' Disp.<> pretty v) (selectedVersion pkginfo) <+> text (replicate (16 - length (prettyShow (pkgName pkginfo))) ' ') <<>> parens pkgkind $+$ nest 4 ( vcat [ entryST "Synopsis" synopsis hideIfNull reflowParagraphs , entry "Versions available" sourceVersions (altText null "[ Not available from server ]") (dispTopVersions 9 (preferredVersions pkginfo)) , entry "Versions installed" installedVersions ( altText null ( if hasLib pkginfo then "[ Not installed ]" else "[ Unknown ]" ) ) (dispTopVersions 4 (preferredVersions pkginfo)) , entryST "Homepage" homepage orNotSpecified text , entryST "Bug reports" bugReports orNotSpecified text , entryST "Description" description hideIfNull reflowParagraphs , entryST "Category" category hideIfNull text , entry "License" license alwaysShow (either pretty pretty) , entryST "Author" author hideIfNull reflowLines , entryST "Maintainer" maintainer hideIfNull reflowLines , entry "Source repo" sourceRepo orNotSpecified text , entry "Executables" executables hideIfNull (commaSep pretty) , entry "Flags" flags hideIfNull (commaSep dispFlag) , entry "Dependencies" dependencies hideIfNull (commaSep dispExtDep) , entry "Documentation" haddockHtml showIfInstalled text , entry "Cached" haveTarball alwaysShow dispYesNo , if not (hasLib pkginfo) then mempty else text "Modules:" $+$ nest 4 (vcat (map pretty . sort . modules $ pkginfo)) ] ) $+$ text "" where entry fname field cond format = case cond (field pkginfo) of Nothing -> label <+> format (field pkginfo) Just Nothing -> mempty Just (Just other) -> label <+> text other where label = text fname Disp.<> char ':' Disp.<> padding padding = text (replicate (13 - length fname) ' ') entryST fname field = entry fname (ShortText.fromShortText . field) normal = Nothing hide = Just Nothing replace msg = Just (Just msg) alwaysShow = const normal hideIfNull v = if null v then hide else normal showIfInstalled v | not isInstalled = hide | null v = replace "[ Not installed ]" | otherwise = normal altText nul msg v = if nul v then replace msg else normal orNotSpecified = altText null "[ Not specified ]" commaSep f = Disp.fsep . Disp.punctuate (Disp.char ',') . map f dispFlag = text . unFlagName . flagName dispYesNo True = text "Yes" dispYesNo False = text "No" dispExtDep (SourceDependency dep) = pretty dep dispExtDep (InstalledDependency dep) = pretty dep isInstalled = not (null (installedVersions pkginfo)) hasExes = length (executables pkginfo) >= 2 -- TODO: exclude non-buildable exes pkgkind | hasLib pkginfo && hasExes = text "programs and library" | hasLib pkginfo && hasExe pkginfo = text "program and library" | hasLib pkginfo = text "library" | hasExes = text "programs" | hasExe pkginfo = text "program" | otherwise = mempty reflowParagraphs :: String -> Doc reflowParagraphs = vcat . intersperse (text "") -- re-insert blank lines . map (fsep . map text . concatMap words) -- reflow paragraphs . filter (/= [""]) . L.groupBy (\x y -> "" `notElem` [x, y]) -- break on blank lines . lines reflowLines :: String -> Doc reflowLines = vcat . map text . lines -- | We get the 'PackageDisplayInfo' by combining the info for the installed -- and available versions of a package. -- -- * We're building info about a various versions of a single named package so -- the input package info records are all supposed to refer to the same -- package name. mergePackageInfo :: VersionRange -> [Installed.InstalledPackageInfo] -> [UnresolvedSourcePackage] -> Maybe UnresolvedSourcePackage -> Bool -> PackageDisplayInfo mergePackageInfo versionPref installedPkgs sourcePkgs selectedPkg showVer = assert (length installedPkgs + length sourcePkgs > 0) $ PackageDisplayInfo { pkgName = combine packageName source packageName installed , selectedVersion = if showVer then fmap packageVersion selectedPkg else Nothing , selectedSourcePkg = sourceSelected , installedVersions = map packageVersion installedPkgs , sourceVersions = map packageVersion sourcePkgs , preferredVersions = versionPref , license = combine Source.licenseRaw source Installed.license installed , maintainer = combine Source.maintainer source Installed.maintainer installed , author = combine Source.author source Installed.author installed , homepage = combine Source.homepage source Installed.homepage installed , bugReports = maybe mempty Source.bugReports source , sourceRepo = fromMaybe mempty . join . fmap ( uncons Nothing Source.repoLocation . sortBy (comparing Source.repoKind) . Source.sourceRepos ) $ source , -- TODO: installed package info is missing synopsis synopsis = maybe mempty Source.synopsis source , description = combine Source.description source Installed.description installed , category = combine Source.category source Installed.category installed , flags = maybe [] Source.genPackageFlags sourceGeneric , hasLib = isJust installed || maybe False (isJust . Source.condLibrary) sourceGeneric , hasExe = maybe False (not . null . Source.condExecutables) sourceGeneric , executables = map fst (maybe [] Source.condExecutables sourceGeneric) , modules = combine (map Installed.exposedName . Installed.exposedModules) installed -- NB: only for the PUBLIC library (concatMap getListOfExposedModules . maybeToList . Source.library) source , dependencies = combine ( map (SourceDependency . simplifyDependency) . Source.allBuildDepends ) source (map InstalledDependency . Installed.depends) installed , haddockHtml = fromMaybe "" . join . fmap (listToMaybe . Installed.haddockHTMLs) $ installed , haveTarball = False } where combine f x g y = fromJust (fmap f x `mplus` fmap g y) installed :: Maybe Installed.InstalledPackageInfo installed = latestWithPref versionPref installedPkgs getListOfExposedModules lib = Source.exposedModules lib ++ map Source.moduleReexportName (Source.reexportedModules lib) sourceSelected | isJust selectedPkg = selectedPkg | otherwise = latestWithPref versionPref sourcePkgs sourceGeneric = fmap srcpkgDescription sourceSelected source = fmap flattenPackageDescription sourceGeneric uncons :: b -> (a -> b) -> [a] -> b uncons z _ [] = z uncons _ f (x : _) = f x -- | Not all the info is pure. We have to check if the docs really are -- installed, because the registered package info lies. Similarly we have to -- check if the tarball has indeed been fetched. updateFileSystemPackageDetails :: PackageDisplayInfo -> IO PackageDisplayInfo updateFileSystemPackageDetails pkginfo = do fetched <- maybe (return False) (isFetched . srcpkgSource) (selectedSourcePkg pkginfo) docsExist <- doesDirectoryExist (haddockHtml pkginfo) return pkginfo { haveTarball = fetched , haddockHtml = if docsExist then haddockHtml pkginfo else "" } latestWithPref :: Package pkg => VersionRange -> [pkg] -> Maybe pkg latestWithPref _ [] = Nothing latestWithPref pref pkgs = Just (maximumBy (comparing prefThenVersion) pkgs) where prefThenVersion pkg = let ver = packageVersion pkg in (withinRange ver pref, ver) -- | Rearrange installed and source packages into groups referring to the -- same package by name. In the result pairs, the lists are guaranteed to not -- both be empty. mergePackages :: [Installed.InstalledPackageInfo] -> [UnresolvedSourcePackage] -> [ ( PackageName , [Installed.InstalledPackageInfo] , [UnresolvedSourcePackage] ) ] mergePackages installedPkgs sourcePkgs = map collect $ mergeBy (\i a -> fst i `compare` fst a) (groupOn packageName installedPkgs) (groupOn packageName sourcePkgs) where collect (OnlyInLeft (name, is)) = (name, is, []) collect (InBoth (_, is) (name, as)) = (name, is, as) collect (OnlyInRight (name, as)) = (name, [], as) groupOn :: Ord key => (a -> key) -> [a] -> [(key, [a])] groupOn key = map (\xs -> (key (head xs), toList xs)) . groupBy (equating key) . sortBy (comparing key) dispTopVersions :: Int -> VersionRange -> [Version] -> Doc dispTopVersions n pref vs = ( Disp.fsep . Disp.punctuate (Disp.char ',') . map (\ver -> if ispref ver then pretty ver else parens (pretty ver)) . sort . take n . interestingVersions ispref $ vs ) <+> trailingMessage where ispref ver = withinRange ver pref extra = length vs - n trailingMessage | extra <= 0 = Disp.empty | otherwise = Disp.parens $ Disp.text "and" <+> Disp.int (length vs - n) <+> if extra == 1 then Disp.text "other" else Disp.text "others" -- | Reorder a bunch of versions to put the most interesting / significant -- versions first. A preferred version range is taken into account. -- -- This may be used in a user interface to select a small number of versions -- to present to the user, e.g. -- -- > let selectVersions = sort . take 5 . interestingVersions pref interestingVersions :: (Version -> Bool) -> [Version] -> [Version] interestingVersions pref = map (mkVersion . fst) . filter snd . concat . Tree.levels . swizzleTree . reorderTree (\(Node (v, _) _) -> pref (mkVersion v)) . reverseTree . mkTree . map (or0 . versionNumbers) where or0 [] = 0 :| [] or0 (x : xs) = x :| xs swizzleTree = unfoldTree (spine []) where spine ts' (Node x []) = (x, ts') spine ts' (Node x (t : ts)) = spine (Node x ts : ts') t reorderTree _ (Node x []) = Node x [] reorderTree p (Node x ts) = Node x (ts' ++ ts'') where (ts', ts'') = partition p (map (reorderTree p) ts) reverseTree (Node x cs) = Node x (reverse (map reverseTree cs)) mkTree :: forall a. Eq a => [NonEmpty a] -> Tree ([a], Bool) mkTree xs = unfoldTree step (False, [], xs) where step :: (Bool, [a], [NonEmpty a]) -> (([a], Bool), [(Bool, [a], [NonEmpty a])]) step (node, ns, vs) = ( (reverse ns, node) , [ (any null vs', n : ns, mapMaybe nonEmpty (toList vs')) | (n, vs') <- groups vs ] ) groups :: [NonEmpty a] -> [(a, NonEmpty [a])] groups = map (\g -> (head (head g), fmap tail g)) . groupBy (equating head)