cabal-install-3.14.1.0: src/Distribution/Client/List.hs
{-# 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)