{-# LANGUAGE DeriveGeneric #-}
module Distribution.Simple.PackageIndex (
InstalledPackageIndex,
PackageIndex,
fromList,
merge,
insert,
deleteUnitId,
deleteSourcePackageId,
deletePackageName,
lookupUnitId,
lookupSourcePackageId,
lookupPackageId,
lookupPackageName,
lookupDependency,
searchByName,
SearchResult(..),
searchByNameSubstring,
allPackages,
allPackagesByName,
allPackagesBySourcePackageId,
brokenPackages,
dependencyClosure,
reverseDependencyClosure,
topologicalOrder,
reverseTopologicalOrder,
dependencyInconsistencies,
dependencyCycles,
dependencyGraph,
moduleNameIndex,
deleteInstalledPackageId,
lookupInstalledPackageId,
) where
import Distribution.Compat.Binary
import Distribution.Compat.Semigroup as Semi
import Distribution.Package
import Distribution.ModuleName
import qualified Distribution.InstalledPackageInfo as IPI
import Distribution.Version
import Distribution.Simple.Utils
import Control.Exception (assert)
import Data.Array ((!))
import qualified Data.Array as Array
import qualified Data.Graph as Graph
import Data.List as List
( null, foldl', sort
, groupBy, sortBy, find, nubBy, deleteBy, deleteFirstsBy )
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (isNothing, fromMaybe)
import qualified Data.Tree as Tree
import GHC.Generics (Generic)
import Prelude hiding (lookup)
data PackageIndex a = PackageIndex
!(Map UnitId a)
!(Map PackageName (Map Version [a]))
deriving (Eq, Generic, Show, Read)
instance Binary a => Binary (PackageIndex a)
type InstalledPackageIndex = PackageIndex IPI.InstalledPackageInfo
instance HasUnitId a => Monoid (PackageIndex a) where
mempty = PackageIndex Map.empty Map.empty
mappend = (Semi.<>)
mconcat [] = mempty
mconcat xs = foldr1 mappend xs
instance HasUnitId a => Semigroup (PackageIndex a) where
(<>) = merge
invariant :: HasUnitId a => PackageIndex a -> Bool
invariant (PackageIndex pids pnames) =
map installedUnitId (Map.elems pids)
== sort
[ assert pinstOk (installedUnitId pinst)
| (pname, pvers) <- Map.toList pnames
, let pversOk = not (Map.null pvers)
, (pver, pinsts) <- assert pversOk $ Map.toList pvers
, let pinsts' = sortBy (comparing installedUnitId) pinsts
pinstsOk = all (\g -> length g == 1)
(groupBy (equating installedUnitId) pinsts')
, pinst <- assert pinstsOk $ pinsts'
, let pinstOk = packageName pinst == pname
&& packageVersion pinst == pver
]
mkPackageIndex :: HasUnitId a
=> Map UnitId a
-> Map PackageName (Map Version [a])
-> PackageIndex a
mkPackageIndex pids pnames = assert (invariant index) index
where index = PackageIndex pids pnames
fromList :: HasUnitId a => [a] -> PackageIndex a
fromList pkgs = mkPackageIndex pids pnames
where
pids = Map.fromList [ (installedUnitId pkg, pkg) | pkg <- pkgs ]
pnames =
Map.fromList
[ (packageName (head pkgsN), pvers)
| pkgsN <- groupBy (equating packageName)
. sortBy (comparing packageId)
$ pkgs
, let pvers =
Map.fromList
[ (packageVersion (head pkgsNV),
nubBy (equating installedUnitId) (reverse pkgsNV))
| pkgsNV <- groupBy (equating packageVersion) pkgsN
]
]
merge :: HasUnitId a => PackageIndex a -> PackageIndex a
-> PackageIndex a
merge (PackageIndex pids1 pnames1) (PackageIndex pids2 pnames2) =
mkPackageIndex (Map.unionWith (\_ y -> y) pids1 pids2)
(Map.unionWith (Map.unionWith mergeBuckets) pnames1 pnames2)
where
mergeBuckets xs ys = ys ++ (xs \\ ys)
(\\) = deleteFirstsBy (equating installedUnitId)
insert :: HasUnitId a => a -> PackageIndex a -> PackageIndex a
insert pkg (PackageIndex pids pnames) =
mkPackageIndex pids' pnames'
where
pids' = Map.insert (installedUnitId pkg) pkg pids
pnames' = insertPackageName pnames
insertPackageName =
Map.insertWith' (\_ -> insertPackageVersion)
(packageName pkg)
(Map.singleton (packageVersion pkg) [pkg])
insertPackageVersion =
Map.insertWith' (\_ -> insertPackageInstance)
(packageVersion pkg) [pkg]
insertPackageInstance pkgs =
pkg : deleteBy (equating installedUnitId) pkg pkgs
deleteUnitId :: HasUnitId a
=> UnitId -> PackageIndex a
-> PackageIndex a
deleteUnitId ipkgid original@(PackageIndex pids pnames) =
case Map.updateLookupWithKey (\_ _ -> Nothing) ipkgid pids of
(Nothing, _) -> original
(Just spkgid, pids') -> mkPackageIndex pids'
(deletePkgName spkgid pnames)
where
deletePkgName spkgid =
Map.update (deletePkgVersion spkgid) (packageName spkgid)
deletePkgVersion spkgid =
(\m -> if Map.null m then Nothing else Just m)
. Map.update deletePkgInstance (packageVersion spkgid)
deletePkgInstance =
(\xs -> if List.null xs then Nothing else Just xs)
. List.deleteBy (\_ pkg -> installedUnitId pkg == ipkgid) undefined
{-# DEPRECATED deleteInstalledPackageId "Use deleteUnitId instead" #-}
deleteInstalledPackageId :: HasUnitId a
=> UnitId -> PackageIndex a
-> PackageIndex a
deleteInstalledPackageId = deleteUnitId
deleteSourcePackageId :: HasUnitId a => PackageId -> PackageIndex a
-> PackageIndex a
deleteSourcePackageId pkgid original@(PackageIndex pids pnames) =
case Map.lookup (packageName pkgid) pnames of
Nothing -> original
Just pvers -> case Map.lookup (packageVersion pkgid) pvers of
Nothing -> original
Just pkgs -> mkPackageIndex
(foldl' (flip (Map.delete . installedUnitId)) pids pkgs)
(deletePkgName pnames)
where
deletePkgName =
Map.update deletePkgVersion (packageName pkgid)
deletePkgVersion =
(\m -> if Map.null m then Nothing else Just m)
. Map.delete (packageVersion pkgid)
deletePackageName :: HasUnitId a => PackageName -> PackageIndex a
-> PackageIndex a
deletePackageName name original@(PackageIndex pids pnames) =
case Map.lookup name pnames of
Nothing -> original
Just pvers -> mkPackageIndex
(foldl' (flip (Map.delete . installedUnitId)) pids
(concat (Map.elems pvers)))
(Map.delete name pnames)
allPackages :: PackageIndex a -> [a]
allPackages (PackageIndex pids _) = Map.elems pids
allPackagesByName :: PackageIndex a -> [(PackageName, [a])]
allPackagesByName (PackageIndex _ pnames) =
[ (pkgname, concat (Map.elems pvers))
| (pkgname, pvers) <- Map.toList pnames ]
allPackagesBySourcePackageId :: HasUnitId a => PackageIndex a
-> [(PackageId, [a])]
allPackagesBySourcePackageId (PackageIndex _ pnames) =
[ (packageId ipkg, ipkgs)
| pvers <- Map.elems pnames
, ipkgs@(ipkg:_) <- Map.elems pvers ]
lookupUnitId :: PackageIndex a -> UnitId
-> Maybe a
lookupUnitId (PackageIndex pids _) pid = Map.lookup pid pids
{-# DEPRECATED lookupInstalledPackageId "Use lookupUnitId instead" #-}
lookupInstalledPackageId :: PackageIndex a -> UnitId
-> Maybe a
lookupInstalledPackageId = lookupUnitId
lookupSourcePackageId :: PackageIndex a -> PackageId -> [a]
lookupSourcePackageId (PackageIndex _ pnames) pkgid =
case Map.lookup (packageName pkgid) pnames of
Nothing -> []
Just pvers -> case Map.lookup (packageVersion pkgid) pvers of
Nothing -> []
Just pkgs -> pkgs
lookupPackageId :: PackageIndex a -> PackageId -> Maybe a
lookupPackageId index pkgid = case lookupSourcePackageId index pkgid of
[] -> Nothing
[pkg] -> Just pkg
_ -> error "Distribution.Simple.PackageIndex: multiple matches found"
lookupPackageName :: PackageIndex a -> PackageName
-> [(Version, [a])]
lookupPackageName (PackageIndex _ pnames) name =
case Map.lookup name pnames of
Nothing -> []
Just pvers -> Map.toList pvers
lookupDependency :: PackageIndex a -> Dependency
-> [(Version, [a])]
lookupDependency (PackageIndex _ pnames) (Dependency name versionRange) =
case Map.lookup name pnames of
Nothing -> []
Just pvers -> [ entry
| entry@(ver, _) <- Map.toList pvers
, ver `withinRange` versionRange ]
searchByName :: PackageIndex a -> String -> SearchResult [a]
searchByName (PackageIndex _ pnames) name =
case [ pkgs | pkgs@(PackageName name',_) <- Map.toList pnames
, lowercase name' == lname ] of
[] -> None
[(_,pvers)] -> Unambiguous (concat (Map.elems pvers))
pkgss -> case find ((PackageName name==) . fst) pkgss of
Just (_,pvers) -> Unambiguous (concat (Map.elems pvers))
Nothing -> Ambiguous (map (concat . Map.elems . snd) pkgss)
where lname = lowercase name
data SearchResult a = None | Unambiguous a | Ambiguous [a]
searchByNameSubstring :: PackageIndex a -> String -> [a]
searchByNameSubstring (PackageIndex _ pnames) searchterm =
[ pkg
| (PackageName name, pvers) <- Map.toList pnames
, lsearchterm `isInfixOf` lowercase name
, pkgs <- Map.elems pvers
, pkg <- pkgs ]
where lsearchterm = lowercase searchterm
dependencyCycles :: PackageInstalled a => PackageIndex a -> [[a]]
dependencyCycles index =
[ vs | Graph.CyclicSCC vs <- Graph.stronglyConnComp adjacencyList ]
where
adjacencyList = [ (pkg, installedUnitId pkg, installedDepends pkg)
| pkg <- allPackages index ]
brokenPackages :: PackageInstalled a => PackageIndex a
-> [(a, [UnitId])]
brokenPackages index =
[ (pkg, missing)
| pkg <- allPackages index
, let missing = [ pkg' | pkg' <- installedDepends pkg
, isNothing (lookupUnitId index pkg') ]
, not (null missing) ]
dependencyClosure :: PackageInstalled a => PackageIndex a
-> [UnitId]
-> Either (PackageIndex a)
[(a, [UnitId])]
dependencyClosure index pkgids0 = case closure mempty [] pkgids0 of
(completed, []) -> Left completed
(completed, _) -> Right (brokenPackages completed)
where
closure completed failed [] = (completed, failed)
closure completed failed (pkgid:pkgids) = case lookupUnitId index pkgid of
Nothing -> closure completed (pkgid:failed) pkgids
Just pkg -> case lookupUnitId completed (installedUnitId pkg) of
Just _ -> closure completed failed pkgids
Nothing -> closure completed' failed pkgids'
where completed' = insert pkg completed
pkgids' = installedDepends pkg ++ pkgids
reverseDependencyClosure :: PackageInstalled a => PackageIndex a
-> [UnitId]
-> [a]
reverseDependencyClosure index =
map vertexToPkg
. concatMap Tree.flatten
. Graph.dfs reverseDepGraph
. map (fromMaybe noSuchPkgId . pkgIdToVertex)
where
(depGraph, vertexToPkg, pkgIdToVertex) = dependencyGraph index
reverseDepGraph = Graph.transposeG depGraph
noSuchPkgId = error "reverseDependencyClosure: package is not in the graph"
topologicalOrder :: PackageInstalled a => PackageIndex a -> [a]
topologicalOrder index = map toPkgId
. Graph.topSort
$ graph
where (graph, toPkgId, _) = dependencyGraph index
reverseTopologicalOrder :: PackageInstalled a => PackageIndex a -> [a]
reverseTopologicalOrder index = map toPkgId
. Graph.topSort
. Graph.transposeG
$ graph
where (graph, toPkgId, _) = dependencyGraph index
dependencyGraph :: PackageInstalled a => PackageIndex a
-> (Graph.Graph,
Graph.Vertex -> a,
UnitId -> Maybe Graph.Vertex)
dependencyGraph index = (graph, vertex_to_pkg, id_to_vertex)
where
graph = Array.listArray bounds
[ [ v | Just v <- map id_to_vertex (installedDepends pkg) ]
| pkg <- pkgs ]
pkgs = sortBy (comparing packageId) (allPackages index)
vertices = zip (map installedUnitId pkgs) [0..]
vertex_map = Map.fromList vertices
id_to_vertex pid = Map.lookup pid vertex_map
vertex_to_pkg vertex = pkgTable ! vertex
pkgTable = Array.listArray bounds pkgs
topBound = length pkgs - 1
bounds = (0, topBound)
dependencyInconsistencies :: PackageInstalled a => PackageIndex a
-> [(PackageName, [(PackageId, Version)])]
dependencyInconsistencies index =
[ (name, [ (pid,packageVersion dep) | (dep,pids) <- uses, pid <- pids])
| (name, ipid_map) <- Map.toList inverseIndex
, let uses = Map.elems ipid_map
, reallyIsInconsistent (map fst uses) ]
where
inverseIndex = Map.fromListWith (Map.unionWith (\(a,b) (_,b') -> (a,b++b')))
[ (packageName dep,
Map.fromList [(ipid,(dep,[packageId pkg]))])
| pkg <- allPackages index
, ipid <- installedDepends pkg
, Just dep <- [lookupUnitId index ipid]
]
reallyIsInconsistent :: PackageInstalled a => [a] -> Bool
reallyIsInconsistent [] = False
reallyIsInconsistent [_p] = False
reallyIsInconsistent [p1, p2] =
let pid1 = installedUnitId p1
pid2 = installedUnitId p2
in pid1 `notElem` installedDepends p2
&& pid2 `notElem` installedDepends p1
reallyIsInconsistent _ = True
moduleNameIndex :: InstalledPackageIndex -> Map ModuleName [IPI.InstalledPackageInfo]
moduleNameIndex index =
Map.fromListWith (++) $ do
pkg <- allPackages index
IPI.ExposedModule m reexport <- IPI.exposedModules pkg
case reexport of
Nothing -> return (m, [pkg])
Just (IPI.OriginalModule _ m') | m == m' -> []
| otherwise -> return (m', [pkg])