It wasn’t why I started playing with Cabal, but after extracting dependencies from a single package I thought struck me that I could extract dependencies from many packages, e.g. hackage, and draw a dependency graph of the result.
The basic idea is to use the code from my earlier post, accumulate dependency information by mapping it over several cabal files. Then convert that information into nodes and edges suitable for building a graph (Data.Graph). That graph is then “graphviz’ed” using Data.Graph.Inductive.Graphviz. Not that since this is performed on Debian Sid I’m using some rather old versions of packages.
First off a shortcut for reading cabal files:
readCabalFile = readPackageDescription silent |
Once I have a GenericPackageDescription I want to collapse it into a regular PackageDescription (see the comments in my previous post for some details regarding this). Then I extract the package name and its dependencies and package them into a tuple. by mapping this function over a list of GenericPackageDescription I end up with an association list where the key is the package and the value is a list of all its dependencies.
processFile gpd = let
finPkgDesc = finalizePackageDescription [] Nothing
"Linux" "X86_64" ("GHC", Version [6, 8, 2] [])
(Right (pd, _)) = finPkgDesc gpd
getPackageName (Dependency name _) = name
nameNDeps = (pkgName . package) &&& (nub . map getPackageName . buildDepends)
in
nameNDeps pd |
In order to create the graph later on I need a complete list of all nodes. To do this I take all the keys and all the values in the association list, collapse them into a single list, and remove duplicates. To turn this resulting list of packages into a list of LNode I then zip it with a list of integers.
getNodes = let
assocKeys = map fst
assocVals = concat . map snd
in zip [1..] . nub . uncurry (++) . (assocKeys &&& assocVals) |
Building the edges is straight forward, but a bit more involved. An edge is a tuple of two integers and something else, I don’t need to label the edges so in my case it is (Int, Int, ()). The list of nodes is basically an association list (an integer for key and a string for value), but I need to flip keys and values since I know the package name and need its node number.
getEdges deps = let
nodes = getNodes deps
nodesAssoc = map (\ (a, b) -> (b, a)) nodes
buildEdges (name, dep) = let
getNode n = fromJust $ lookup n nodesAssoc
fromNode = getNode name
in map (\ t -> (fromNode, getNode t, ())) dep
in concat $ map buildEdges deps |
Now that that’s done I can put it all together in a main function. The trickiest bit of that was to find the size of A4 in incehs
main = do
files <- getArgs
gpds <- mapM readCabalFile files
let deps = map processFile gpds
let (nodes, edges) = (getNodes &&& getEdges) deps
let depGraph = mkGraph nodes edges :: Gr String ()
putStrLn $ graphviz depGraph "Dependency_Graph" (11.7, 16.5) (1,1) Landscape |
I downloaded all cabal files from Hackage and ran this code over them. I bumped into a few that use Cabal features not supported in the ancient version I’m using. I was a bit disappointed that Cabal wouldn’t let me handle that kind of errors myself (as I already expressed in an earlier post) so I was forced to delete them manually.
Here’s the output, completely unreadable, I know, but still sort of cool.