xref: /haiku/3rdparty/pulkomandy/pkggraph.py (revision eea5774f46bba925156498abf9cb1a1165647bf7)
1#!python3
2#
3# Copyright 2019-2023 Adrien Destugues <pulkomandy@pulkomandy.tk>
4#
5# Distributed under terms of the MIT license.
6
7from os import listdir
8from os.path import isfile, join
9import subprocess
10import re
11import sys
12
13"""
14Generate a graph of dependencies for a set of packages packages in an Haiku system.
15
16Usage:
17- Without arguments: generate a graph of all packages in /system/packages. This can be quite busy
18  and hard to understand. It will also take a while to generate the graph, as dot tries to route
19  thousands of edges.
20- With arguments: the arguments are a list of packages to analyze. This allows to print a subset
21  of the packages for a better view.
22
23Dependencies are resolved: if a package has a specific string in its REQUIRES and another has the
24same string in its PROVIDES, a BLUE edge is drawn between the two package.
25If a package has a REQUIRES that is not matched by any other package in the set, this REQUIRE entry
26is drawn as a node, and the edge pointing to it is RED (so you can easily see missing dependencies
27in a package subset). If you use the complete /system/packages hierarchy, there should be no red
28edges, all dependencies are satisfied.
29
30The output of the script can be saved to a file for manual analysis (for example, you can search
31packages that nothing points to, and see if you want to uninstall them), or piped into dot for
32rendering as a PNG, for example:
33
34    cd /system/packages
35    pkggraph.py qt* gst_plugins_ba* | dot -Tpng -o /tmp/packages.png
36    ShowImage /tmp/packages.png
37"""
38
39# Collect the list of packages to be analyzed
40path = "/system/packages"
41if len(sys.argv) > 1:
42    packages = sys.argv[1:]
43else:
44    packages = [join(path, f) for f in listdir(path) if(isfile(join(path, f)))]
45
46
47# List the provides and requires for each package
48# pmap maps any provides to the corresponding packagename
49# rmap maps a packagename to the corresponding requires
50pmap = {}
51rmap = {}
52
53for p in packages:
54    pkgtool = subprocess.Popen(['package', 'list', '-i', p], stdout = subprocess.PIPE)
55    infos, stderr = pkgtool.communicate()
56
57    provides = []
58    requires = []
59
60    for line in infos.split(b'\n'):
61        if line.startswith(b"\tprovides:"):
62            provides.append(line.split(b' ')[1])
63        if line.startswith(b"\trequires:"):
64            line = line.split(b' ')[1]
65            if b'>' in line:
66                line = line.split(b'>')[0]
67            if b'=' in line:
68                line = line.split(b'=')[0]
69            if line != b'haiku' and line != b'haiku_x86':
70                requires.append(line)
71
72    basename = provides[0]
73    # Merge devel packages with the parent package
74    basename = basename.decode("utf-8").removesuffix("_devel")
75    for pro in provides:
76        pmap[pro] = basename
77    if len(requires) > 0:
78        if basename not in rmap:
79            rmap[basename] = requires
80        else:
81            rmap[basename].extend(requires)
82
83# Generate the graph in dot/graphviz format
84# For each package, there is an edge to each dependency package
85print('strict digraph {\nrankdir="LR"\nsplines=ortho\nnode [ fontname="Noto", fontsize=10];')
86
87for name, dependencies in rmap.items():
88    for dep in dependencies:
89        color = "red"
90        if dep in pmap:
91            dep = pmap[dep]
92            color = "blue"
93        print(f'"{name}" -> "{dep}" [color={color}]')
94
95print("}")
96