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