1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun 3*4882a593Smuzhiyun# Usage (the graphviz package must be installed in your distribution) 4*4882a593Smuzhiyun# ./support/scripts/graph-depends [-p package-name] > test.dot 5*4882a593Smuzhiyun# dot -Tpdf test.dot -o test.pdf 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun# With no arguments, graph-depends will draw a complete graph of 8*4882a593Smuzhiyun# dependencies for the current configuration. 9*4882a593Smuzhiyun# If '-p <package-name>' is specified, graph-depends will draw a graph 10*4882a593Smuzhiyun# of dependencies for the given package name. 11*4882a593Smuzhiyun# If '-d <depth>' is specified, graph-depends will limit the depth of 12*4882a593Smuzhiyun# the dependency graph to 'depth' levels. 13*4882a593Smuzhiyun# 14*4882a593Smuzhiyun# Limitations 15*4882a593Smuzhiyun# 16*4882a593Smuzhiyun# * Some packages have dependencies that depend on the Buildroot 17*4882a593Smuzhiyun# configuration. For example, many packages have a dependency on 18*4882a593Smuzhiyun# openssl if openssl has been enabled. This tool will graph the 19*4882a593Smuzhiyun# dependencies as they are with the current Buildroot 20*4882a593Smuzhiyun# configuration. 21*4882a593Smuzhiyun# 22*4882a593Smuzhiyun# Copyright (C) 2010-2013 Thomas Petazzoni <thomas.petazzoni@free-electrons.com> 23*4882a593Smuzhiyun# Copyright (C) 2019 Yann E. MORIN <yann.morin.1998@free.fr> 24*4882a593Smuzhiyun 25*4882a593Smuzhiyunimport logging 26*4882a593Smuzhiyunimport sys 27*4882a593Smuzhiyunimport argparse 28*4882a593Smuzhiyunfrom fnmatch import fnmatch 29*4882a593Smuzhiyun 30*4882a593Smuzhiyunimport brpkgutil 31*4882a593Smuzhiyun 32*4882a593Smuzhiyun# Modes of operation: 33*4882a593SmuzhiyunMODE_FULL = 1 # draw full dependency graph for all selected packages 34*4882a593SmuzhiyunMODE_PKG = 2 # draw dependency graph for a given package 35*4882a593Smuzhiyun 36*4882a593Smuzhiyunallpkgs = [] 37*4882a593Smuzhiyun 38*4882a593Smuzhiyun 39*4882a593Smuzhiyun# The Graphviz "dot" utility doesn't like dashes in node names. So for 40*4882a593Smuzhiyun# node names, we strip all dashes. Also, nodes can't start with a number, 41*4882a593Smuzhiyun# so we prepend an underscore. 42*4882a593Smuzhiyundef pkg_node_name(pkg): 43*4882a593Smuzhiyun return "_" + pkg.replace("-", "") 44*4882a593Smuzhiyun 45*4882a593Smuzhiyun 46*4882a593Smuzhiyun# Basic cache for the results of the is_dep() function, in order to 47*4882a593Smuzhiyun# optimize the execution time. The cache is a dict of dict of boolean 48*4882a593Smuzhiyun# values. The key to the primary dict is "pkg", and the key of the 49*4882a593Smuzhiyun# sub-dicts is "pkg2". 50*4882a593Smuzhiyunis_dep_cache = {} 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun 53*4882a593Smuzhiyundef is_dep_cache_insert(pkg, pkg2, val): 54*4882a593Smuzhiyun try: 55*4882a593Smuzhiyun is_dep_cache[pkg].update({pkg2: val}) 56*4882a593Smuzhiyun except KeyError: 57*4882a593Smuzhiyun is_dep_cache[pkg] = {pkg2: val} 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun# Retrieves from the cache whether pkg2 is a transitive dependency 61*4882a593Smuzhiyun# of pkg. 62*4882a593Smuzhiyun# Note: raises a KeyError exception if the dependency is not known. 63*4882a593Smuzhiyundef is_dep_cache_lookup(pkg, pkg2): 64*4882a593Smuzhiyun return is_dep_cache[pkg][pkg2] 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun 67*4882a593Smuzhiyun# This function return True if pkg is a dependency (direct or 68*4882a593Smuzhiyun# transitive) of pkg2, dependencies being listed in the deps 69*4882a593Smuzhiyun# dictionary. Returns False otherwise. 70*4882a593Smuzhiyun# This is the un-cached version. 71*4882a593Smuzhiyundef is_dep_uncached(pkg, pkg2, deps): 72*4882a593Smuzhiyun try: 73*4882a593Smuzhiyun for p in deps[pkg2]: 74*4882a593Smuzhiyun if pkg == p: 75*4882a593Smuzhiyun return True 76*4882a593Smuzhiyun if is_dep(pkg, p, deps): 77*4882a593Smuzhiyun return True 78*4882a593Smuzhiyun except KeyError: 79*4882a593Smuzhiyun pass 80*4882a593Smuzhiyun return False 81*4882a593Smuzhiyun 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun# See is_dep_uncached() above; this is the cached version. 84*4882a593Smuzhiyundef is_dep(pkg, pkg2, deps): 85*4882a593Smuzhiyun try: 86*4882a593Smuzhiyun return is_dep_cache_lookup(pkg, pkg2) 87*4882a593Smuzhiyun except KeyError: 88*4882a593Smuzhiyun val = is_dep_uncached(pkg, pkg2, deps) 89*4882a593Smuzhiyun is_dep_cache_insert(pkg, pkg2, val) 90*4882a593Smuzhiyun return val 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun 93*4882a593Smuzhiyun# This function eliminates transitive dependencies; for example, given 94*4882a593Smuzhiyun# these dependency chain: A->{B,C} and B->{C}, the A->{C} dependency is 95*4882a593Smuzhiyun# already covered by B->{C}, so C is a transitive dependency of A, via B. 96*4882a593Smuzhiyun# The functions does: 97*4882a593Smuzhiyun# - for each dependency d[i] of the package pkg 98*4882a593Smuzhiyun# - if d[i] is a dependency of any of the other dependencies d[j] 99*4882a593Smuzhiyun# - do not keep d[i] 100*4882a593Smuzhiyun# - otherwise keep d[i] 101*4882a593Smuzhiyundef remove_transitive_deps(pkg, deps): 102*4882a593Smuzhiyun d = deps[pkg] 103*4882a593Smuzhiyun new_d = [] 104*4882a593Smuzhiyun for i in range(len(d)): 105*4882a593Smuzhiyun keep_me = True 106*4882a593Smuzhiyun for j in range(len(d)): 107*4882a593Smuzhiyun if j == i: 108*4882a593Smuzhiyun continue 109*4882a593Smuzhiyun if is_dep(d[i], d[j], deps): 110*4882a593Smuzhiyun keep_me = False 111*4882a593Smuzhiyun if keep_me: 112*4882a593Smuzhiyun new_d.append(d[i]) 113*4882a593Smuzhiyun return new_d 114*4882a593Smuzhiyun 115*4882a593Smuzhiyun 116*4882a593Smuzhiyun# List of dependencies that all/many packages have, and that we want 117*4882a593Smuzhiyun# to trim when generating the dependency graph. 118*4882a593SmuzhiyunMANDATORY_DEPS = ['toolchain', 'skeleton', 'host-skeleton', 'host-tar', 'host-gzip', 'host-ccache'] 119*4882a593Smuzhiyun 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun# This function removes the dependency on some 'mandatory' package, like the 122*4882a593Smuzhiyun# 'toolchain' package, or the 'skeleton' package 123*4882a593Smuzhiyundef remove_mandatory_deps(pkg, deps): 124*4882a593Smuzhiyun return [p for p in deps[pkg] if p not in MANDATORY_DEPS] 125*4882a593Smuzhiyun 126*4882a593Smuzhiyun 127*4882a593Smuzhiyun# This function returns all dependencies of pkg that are part of the 128*4882a593Smuzhiyun# mandatory dependencies: 129*4882a593Smuzhiyundef get_mandatory_deps(pkg, deps): 130*4882a593Smuzhiyun return [p for p in deps[pkg] if p in MANDATORY_DEPS] 131*4882a593Smuzhiyun 132*4882a593Smuzhiyun 133*4882a593Smuzhiyun# This function will check that there is no loop in the dependency chain 134*4882a593Smuzhiyun# As a side effect, it builds up the dependency cache. 135*4882a593Smuzhiyundef check_circular_deps(deps): 136*4882a593Smuzhiyun def recurse(pkg): 137*4882a593Smuzhiyun if pkg not in list(deps.keys()): 138*4882a593Smuzhiyun return 139*4882a593Smuzhiyun if pkg in not_loop: 140*4882a593Smuzhiyun return 141*4882a593Smuzhiyun not_loop.append(pkg) 142*4882a593Smuzhiyun chain.append(pkg) 143*4882a593Smuzhiyun for p in deps[pkg]: 144*4882a593Smuzhiyun if p in chain: 145*4882a593Smuzhiyun logging.warning("\nRecursion detected for : %s" % (p)) 146*4882a593Smuzhiyun while True: 147*4882a593Smuzhiyun _p = chain.pop() 148*4882a593Smuzhiyun logging.warning("which is a dependency of: %s" % (_p)) 149*4882a593Smuzhiyun if p == _p: 150*4882a593Smuzhiyun sys.exit(1) 151*4882a593Smuzhiyun recurse(p) 152*4882a593Smuzhiyun chain.pop() 153*4882a593Smuzhiyun 154*4882a593Smuzhiyun not_loop = [] 155*4882a593Smuzhiyun chain = [] 156*4882a593Smuzhiyun for pkg in list(deps.keys()): 157*4882a593Smuzhiyun recurse(pkg) 158*4882a593Smuzhiyun 159*4882a593Smuzhiyun 160*4882a593Smuzhiyun# This functions trims down the dependency list of all packages. 161*4882a593Smuzhiyun# It applies in sequence all the dependency-elimination methods. 162*4882a593Smuzhiyundef remove_extra_deps(deps, rootpkg, transitive, arrow_dir): 163*4882a593Smuzhiyun # For the direct dependencies, find and eliminate mandatory 164*4882a593Smuzhiyun # deps, and add them to the root package. Don't do it for a 165*4882a593Smuzhiyun # reverse graph, because mandatory deps are only direct deps. 166*4882a593Smuzhiyun if arrow_dir == "forward": 167*4882a593Smuzhiyun for pkg in list(deps.keys()): 168*4882a593Smuzhiyun if not pkg == rootpkg: 169*4882a593Smuzhiyun for d in get_mandatory_deps(pkg, deps): 170*4882a593Smuzhiyun if d not in deps[rootpkg]: 171*4882a593Smuzhiyun deps[rootpkg].append(d) 172*4882a593Smuzhiyun deps[pkg] = remove_mandatory_deps(pkg, deps) 173*4882a593Smuzhiyun for pkg in list(deps.keys()): 174*4882a593Smuzhiyun if not transitive or pkg == rootpkg: 175*4882a593Smuzhiyun deps[pkg] = remove_transitive_deps(pkg, deps) 176*4882a593Smuzhiyun return deps 177*4882a593Smuzhiyun 178*4882a593Smuzhiyun 179*4882a593Smuzhiyun# Print the attributes of a node: label and fill-color 180*4882a593Smuzhiyundef print_attrs(outfile, pkg, pkg_type, pkg_version, depth, colors): 181*4882a593Smuzhiyun name = pkg_node_name(pkg) 182*4882a593Smuzhiyun if pkg == 'all': 183*4882a593Smuzhiyun label = 'ALL' 184*4882a593Smuzhiyun else: 185*4882a593Smuzhiyun label = pkg 186*4882a593Smuzhiyun if depth == 0: 187*4882a593Smuzhiyun color = colors[0] 188*4882a593Smuzhiyun else: 189*4882a593Smuzhiyun if pkg_type == "host": 190*4882a593Smuzhiyun color = colors[2] 191*4882a593Smuzhiyun else: 192*4882a593Smuzhiyun color = colors[1] 193*4882a593Smuzhiyun if pkg_version == "virtual": 194*4882a593Smuzhiyun outfile.write("%s [label = <<I>%s</I>>]\n" % (name, label)) 195*4882a593Smuzhiyun else: 196*4882a593Smuzhiyun outfile.write("%s [label = \"%s\"]\n" % (name, label)) 197*4882a593Smuzhiyun outfile.write("%s [color=%s,style=filled]\n" % (name, color)) 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun 200*4882a593Smuzhiyundone_deps = [] 201*4882a593Smuzhiyun 202*4882a593Smuzhiyun 203*4882a593Smuzhiyun# Print the dependency graph of a package 204*4882a593Smuzhiyundef print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list, 205*4882a593Smuzhiyun arrow_dir, draw_graph, depth, max_depth, pkg, colors): 206*4882a593Smuzhiyun if pkg in done_deps: 207*4882a593Smuzhiyun return 208*4882a593Smuzhiyun done_deps.append(pkg) 209*4882a593Smuzhiyun if draw_graph: 210*4882a593Smuzhiyun print_attrs(outfile, pkg, dict_types[pkg], dict_versions[pkg], depth, colors) 211*4882a593Smuzhiyun elif depth != 0: 212*4882a593Smuzhiyun outfile.write("%s " % pkg) 213*4882a593Smuzhiyun if pkg not in dict_deps: 214*4882a593Smuzhiyun return 215*4882a593Smuzhiyun for p in stop_list: 216*4882a593Smuzhiyun if fnmatch(pkg, p): 217*4882a593Smuzhiyun return 218*4882a593Smuzhiyun if dict_versions[pkg] == "virtual" and "virtual" in stop_list: 219*4882a593Smuzhiyun return 220*4882a593Smuzhiyun if dict_types[pkg] == "host" and "host" in stop_list: 221*4882a593Smuzhiyun return 222*4882a593Smuzhiyun if max_depth == 0 or depth < max_depth: 223*4882a593Smuzhiyun for d in dict_deps[pkg]: 224*4882a593Smuzhiyun if dict_versions[d] == "virtual" and "virtual" in exclude_list: 225*4882a593Smuzhiyun continue 226*4882a593Smuzhiyun if dict_types[d] == "host" and "host" in exclude_list: 227*4882a593Smuzhiyun continue 228*4882a593Smuzhiyun add = True 229*4882a593Smuzhiyun for p in exclude_list: 230*4882a593Smuzhiyun if fnmatch(d, p): 231*4882a593Smuzhiyun add = False 232*4882a593Smuzhiyun break 233*4882a593Smuzhiyun if add: 234*4882a593Smuzhiyun if draw_graph: 235*4882a593Smuzhiyun outfile.write("%s -> %s [dir=%s]\n" % (pkg_node_name(pkg), pkg_node_name(d), arrow_dir)) 236*4882a593Smuzhiyun print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list, 237*4882a593Smuzhiyun arrow_dir, draw_graph, depth + 1, max_depth, d, colors) 238*4882a593Smuzhiyun 239*4882a593Smuzhiyun 240*4882a593Smuzhiyundef parse_args(): 241*4882a593Smuzhiyun parser = argparse.ArgumentParser(description="Graph packages dependencies") 242*4882a593Smuzhiyun parser.add_argument("--check-only", "-C", dest="check_only", action="store_true", default=False, 243*4882a593Smuzhiyun help="Only do the dependency checks (circular deps...)") 244*4882a593Smuzhiyun parser.add_argument("--outfile", "-o", metavar="OUT_FILE", dest="outfile", 245*4882a593Smuzhiyun help="File in which to generate the dot representation") 246*4882a593Smuzhiyun parser.add_argument("--package", '-p', metavar="PACKAGE", 247*4882a593Smuzhiyun help="Graph the dependencies of PACKAGE") 248*4882a593Smuzhiyun parser.add_argument("--depth", '-d', metavar="DEPTH", dest="depth", type=int, default=0, 249*4882a593Smuzhiyun help="Limit the dependency graph to DEPTH levels; 0 means no limit.") 250*4882a593Smuzhiyun parser.add_argument("--stop-on", "-s", metavar="PACKAGE", dest="stop_list", action="append", 251*4882a593Smuzhiyun help="Do not graph past this package (can be given multiple times)." + 252*4882a593Smuzhiyun " Can be a package name or a glob, " + 253*4882a593Smuzhiyun " 'virtual' to stop on virtual packages, or " + 254*4882a593Smuzhiyun "'host' to stop on host packages.") 255*4882a593Smuzhiyun parser.add_argument("--exclude", "-x", metavar="PACKAGE", dest="exclude_list", action="append", 256*4882a593Smuzhiyun help="Like --stop-on, but do not add PACKAGE to the graph.") 257*4882a593Smuzhiyun parser.add_argument("--exclude-mandatory", "-X", action="store_true", 258*4882a593Smuzhiyun help="Like if -x was passed for all mandatory dependencies.") 259*4882a593Smuzhiyun parser.add_argument("--colors", "-c", metavar="COLOR_LIST", dest="colors", 260*4882a593Smuzhiyun default="lightblue,grey,gainsboro", 261*4882a593Smuzhiyun help="Comma-separated list of the three colors to use" + 262*4882a593Smuzhiyun " to draw the top-level package, the target" + 263*4882a593Smuzhiyun " packages, and the host packages, in this order." + 264*4882a593Smuzhiyun " Defaults to: 'lightblue,grey,gainsboro'") 265*4882a593Smuzhiyun parser.add_argument("--transitive", dest="transitive", action='store_true', 266*4882a593Smuzhiyun default=False) 267*4882a593Smuzhiyun parser.add_argument("--no-transitive", dest="transitive", action='store_false', 268*4882a593Smuzhiyun help="Draw (do not draw) transitive dependencies") 269*4882a593Smuzhiyun parser.add_argument("--direct", dest="direct", action='store_true', default=True, 270*4882a593Smuzhiyun help="Draw direct dependencies (the default)") 271*4882a593Smuzhiyun parser.add_argument("--reverse", dest="direct", action='store_false', 272*4882a593Smuzhiyun help="Draw reverse dependencies") 273*4882a593Smuzhiyun parser.add_argument("--quiet", '-q', dest="quiet", action='store_true', 274*4882a593Smuzhiyun help="Quiet") 275*4882a593Smuzhiyun parser.add_argument("--flat-list", '-f', dest="flat_list", action='store_true', default=False, 276*4882a593Smuzhiyun help="Do not draw graph, just print a flat list") 277*4882a593Smuzhiyun return parser.parse_args() 278*4882a593Smuzhiyun 279*4882a593Smuzhiyun 280*4882a593Smuzhiyundef main(): 281*4882a593Smuzhiyun args = parse_args() 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun check_only = args.check_only 284*4882a593Smuzhiyun 285*4882a593Smuzhiyun logging.basicConfig(stream=sys.stderr, format='%(message)s', 286*4882a593Smuzhiyun level=logging.WARNING if args.quiet else logging.INFO) 287*4882a593Smuzhiyun 288*4882a593Smuzhiyun if args.outfile is None: 289*4882a593Smuzhiyun outfile = sys.stdout 290*4882a593Smuzhiyun else: 291*4882a593Smuzhiyun if check_only: 292*4882a593Smuzhiyun logging.error("don't specify outfile and check-only at the same time") 293*4882a593Smuzhiyun sys.exit(1) 294*4882a593Smuzhiyun outfile = open(args.outfile, "w") 295*4882a593Smuzhiyun 296*4882a593Smuzhiyun if args.package is None: 297*4882a593Smuzhiyun mode = MODE_FULL 298*4882a593Smuzhiyun rootpkg = 'all' 299*4882a593Smuzhiyun else: 300*4882a593Smuzhiyun mode = MODE_PKG 301*4882a593Smuzhiyun rootpkg = args.package 302*4882a593Smuzhiyun 303*4882a593Smuzhiyun if args.stop_list is None: 304*4882a593Smuzhiyun stop_list = [] 305*4882a593Smuzhiyun else: 306*4882a593Smuzhiyun stop_list = args.stop_list 307*4882a593Smuzhiyun 308*4882a593Smuzhiyun if args.exclude_list is None: 309*4882a593Smuzhiyun exclude_list = [] 310*4882a593Smuzhiyun else: 311*4882a593Smuzhiyun exclude_list = args.exclude_list 312*4882a593Smuzhiyun 313*4882a593Smuzhiyun if args.exclude_mandatory: 314*4882a593Smuzhiyun exclude_list += MANDATORY_DEPS 315*4882a593Smuzhiyun 316*4882a593Smuzhiyun if args.direct: 317*4882a593Smuzhiyun arrow_dir = "forward" 318*4882a593Smuzhiyun else: 319*4882a593Smuzhiyun if mode == MODE_FULL: 320*4882a593Smuzhiyun logging.error("--reverse needs a package") 321*4882a593Smuzhiyun sys.exit(1) 322*4882a593Smuzhiyun arrow_dir = "back" 323*4882a593Smuzhiyun 324*4882a593Smuzhiyun draw_graph = not args.flat_list 325*4882a593Smuzhiyun 326*4882a593Smuzhiyun # Get the colors: we need exactly three colors, 327*4882a593Smuzhiyun # so no need not split more than 4 328*4882a593Smuzhiyun # We'll let 'dot' validate the colors... 329*4882a593Smuzhiyun colors = args.colors.split(',', 4) 330*4882a593Smuzhiyun if len(colors) != 3: 331*4882a593Smuzhiyun logging.error("Error: incorrect color list '%s'" % args.colors) 332*4882a593Smuzhiyun sys.exit(1) 333*4882a593Smuzhiyun 334*4882a593Smuzhiyun deps, rdeps, dict_types, dict_versions = brpkgutil.get_dependency_tree() 335*4882a593Smuzhiyun dict_deps = deps if args.direct else rdeps 336*4882a593Smuzhiyun 337*4882a593Smuzhiyun check_circular_deps(dict_deps) 338*4882a593Smuzhiyun if check_only: 339*4882a593Smuzhiyun sys.exit(0) 340*4882a593Smuzhiyun 341*4882a593Smuzhiyun dict_deps = remove_extra_deps(dict_deps, rootpkg, args.transitive, arrow_dir) 342*4882a593Smuzhiyun 343*4882a593Smuzhiyun # Start printing the graph data 344*4882a593Smuzhiyun if draw_graph: 345*4882a593Smuzhiyun outfile.write("digraph G {\n") 346*4882a593Smuzhiyun 347*4882a593Smuzhiyun print_pkg_deps(outfile, dict_deps, dict_types, dict_versions, stop_list, exclude_list, 348*4882a593Smuzhiyun arrow_dir, draw_graph, 0, args.depth, rootpkg, colors) 349*4882a593Smuzhiyun 350*4882a593Smuzhiyun if draw_graph: 351*4882a593Smuzhiyun outfile.write("}\n") 352*4882a593Smuzhiyun else: 353*4882a593Smuzhiyun outfile.write("\n") 354*4882a593Smuzhiyun 355*4882a593Smuzhiyun 356*4882a593Smuzhiyunif __name__ == "__main__": 357*4882a593Smuzhiyun sys.exit(main()) 358