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
39path = "/system/packages"
40if len(sys.argv) > 1:
41    packages = sys.argv[1:]
42else:
43    packages = [join(path, f) for f in listdir(path) if(isfile(join(path, f)))]
44
45print('strict digraph {\nrankdir="LR"\nsplines=ortho\nnode [ fontname="Noto", fontsize=10];')
46
47pmap = {}
48rmap = {}
49
50for p in packages:
51    pkgtool = subprocess.Popen(['package', 'list', '-i', p], stdout = subprocess.PIPE)
52    infos, stderr = pkgtool.communicate()
53
54    provides = []
55    requires = []
56
57    for line in infos.split(b'\n'):
58        if line.startswith(b"\tprovides:"):
59            provides.append(line.split(b' ')[1])
60        if line.startswith(b"\trequires:"):
61            line = line.split(b' ')[1]
62            if b'>' in line:
63                line = line.split(b'>')[0]
64            if b'=' in line:
65                line = line.split(b'=')[0]
66            if line != b'haiku' and line != b'haiku_x86':
67                requires.append(line)
68
69    for pro in provides:
70        pmap[pro] = provides[0]
71    if len(requires) > 0:
72        rmap[provides[0]] = requires
73
74for k,v in rmap.items():
75    for dep in v:
76        color = "red"
77        if dep in pmap:
78            dep = pmap[dep]
79            color = "blue"
80        print('"%s" -> "%s" [color=%s]' % (k.decode('utf-8'), dep.decode('utf-8'), color))
81
82print("}")
83