123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- #!/usr/bin/python
- # Usage (the graphviz package must be installed in your distribution)
- # ./support/scripts/graph-depends [-p package-name] > test.dot
- # dot -Tpdf test.dot -o test.pdf
- #
- # With no arguments, graph-depends will draw a complete graph of
- # dependencies for the current configuration.
- # If '-p <package-name>' is specified, graph-depends will draw a graph
- # of dependencies for the given package name.
- # If '-d <depth>' is specified, graph-depends will limit the depth of
- # the dependency graph to 'depth' levels.
- #
- # Limitations
- #
- # * Some packages have dependencies that depend on the Buildroot
- # configuration. For example, many packages have a dependency on
- # openssl if openssl has been enabled. This tool will graph the
- # dependencies as they are with the current Buildroot
- # configuration.
- #
- # Copyright (C) 2010-2013 Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
- import sys
- import subprocess
- import argparse
- # Modes of operation:
- MODE_FULL = 1 # draw full dependency graph for all selected packages
- MODE_PKG = 2 # draw dependency graph for a given package
- mode = 0
- # Limit drawing the dependency graph to this depth. 0 means 'no limit'.
- max_depth = 0
- # Whether to draw the transitive dependencies
- transitive = True
- parser = argparse.ArgumentParser(description="Graph pacakges dependencies")
- parser.add_argument("--package", '-p', metavar="PACKAGE",
- help="Graph the dependencies of PACKAGE")
- parser.add_argument("--depth", '-d', metavar="DEPTH", dest="depth", type=int, default=0,
- help="Limit the dependency graph to DEPTH levels; 0 means no limit.")
- parser.add_argument("--colours", "-c", metavar="COLOR_LIST", dest="colours",
- default="lightblue,grey,gainsboro",
- help="Comma-separated list of the three colours to use" \
- + " to draw the top-level package, the target" \
- + " packages, and the host packages, in this order." \
- + " Defaults to: 'lightblue,grey,gainsboro'")
- parser.add_argument("--transitive", dest="transitive", action='store_true',
- default=False)
- parser.add_argument("--no-transitive", dest="transitive", action='store_false',
- help="Draw (do not draw) transitive dependencies")
- args = parser.parse_args()
- if args.package is None:
- mode = MODE_FULL
- else:
- mode = MODE_PKG
- rootpkg = args.package
- max_depth = args.depth
- transitive = args.transitive
- # Get the colours: we need exactly three colours,
- # so no need not split more than 4
- # We'll let 'dot' validate the colours...
- colours = args.colours.split(',',4)
- if len(colours) != 3:
- sys.stderr.write("Error: incorrect colour list '%s'\n" % args.colours)
- sys.exit(1)
- root_colour = colours[0]
- target_colour = colours[1]
- host_colour = colours[2]
- allpkgs = []
- # Execute the "make show-targets" command to get the list of the main
- # Buildroot TARGETS and return it formatted as a Python list. This
- # list is used as the starting point for full dependency graphs
- def get_targets():
- sys.stderr.write("Getting targets\n")
- cmd = ["make", "-s", "--no-print-directory", "show-targets"]
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
- output = p.communicate()[0].strip()
- if p.returncode != 0:
- return None
- if output == '':
- return []
- return output.split(' ')
- # Execute the "make <pkg>-show-depends" command to get the list of
- # dependencies of a given list of packages, and return the list of
- # dependencies formatted as a Python dictionary.
- def get_depends(pkgs):
- sys.stderr.write("Getting dependencies for %s\n" % pkgs)
- cmd = ["make", "-s", "--no-print-directory" ]
- for pkg in pkgs:
- cmd.append("%s-show-depends" % pkg)
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
- output = p.communicate()[0]
- if p.returncode != 0:
- sys.stderr.write("Error getting dependencies %s\n" % pkgs)
- sys.exit(1)
- output = output.split("\n")
- if len(output) != len(pkgs) + 1:
- sys.stderr.write("Error getting dependencies\n")
- sys.exit(1)
- deps = {}
- for i in range(0, len(pkgs)):
- pkg = pkgs[i]
- pkg_deps = output[i].split(" ")
- if pkg_deps == ['']:
- deps[pkg] = []
- else:
- deps[pkg] = pkg_deps
- return deps
- # Recursive function that builds the tree of dependencies for a given
- # list of packages. The dependencies are built in a list called
- # 'dependencies', which contains tuples of the form (pkg1 ->
- # pkg2_on_which_pkg1_depends, pkg3 -> pkg4_on_which_pkg3_depends) and
- # the function finally returns this list.
- def get_all_depends(pkgs):
- dependencies = []
- # Filter the packages for which we already have the dependencies
- filtered_pkgs = []
- for pkg in pkgs:
- if pkg in allpkgs:
- continue
- filtered_pkgs.append(pkg)
- allpkgs.append(pkg)
- if len(filtered_pkgs) == 0:
- return []
- depends = get_depends(filtered_pkgs)
- deps = set()
- for pkg in filtered_pkgs:
- pkg_deps = depends[pkg]
- # This package has no dependency.
- if pkg_deps == []:
- continue
- # Add dependencies to the list of dependencies
- for dep in pkg_deps:
- dependencies.append((pkg, dep))
- deps.add(dep)
- if len(deps) != 0:
- newdeps = get_all_depends(deps)
- if newdeps is not None:
- dependencies += newdeps
- return dependencies
- # The Graphviz "dot" utility doesn't like dashes in node names. So for
- # node names, we strip all dashes.
- def pkg_node_name(pkg):
- return pkg.replace("-","")
- TARGET_EXCEPTIONS = [
- "target-generic-securetty",
- "target-generic-issue",
- "target-generic-getty-busybox",
- "target-generic-do-remount-rw",
- "target-generic-dont-remount-rw",
- "target-finalize",
- "erase-fakeroots",
- "target-generic-hostname",
- "target-root-passwd",
- "target-post-image",
- "target-purgelocales",
- ]
- # In full mode, start with the result of get_targets() to get the main
- # targets and then use get_all_depends() for all targets
- if mode == MODE_FULL:
- targets = get_targets()
- dependencies = []
- allpkgs.append('all')
- filtered_targets = []
- for tg in targets:
- # Skip uninteresting targets
- if tg in TARGET_EXCEPTIONS:
- continue
- dependencies.append(('all', tg))
- filtered_targets.append(tg)
- deps = get_all_depends(filtered_targets)
- if deps is not None:
- dependencies += deps
- rootpkg = 'all'
- # In pkg mode, start directly with get_all_depends() on the requested
- # package
- elif mode == MODE_PKG:
- dependencies = get_all_depends([rootpkg])
- # Make the dependencies a dictionnary { 'pkg':[dep1, dep2, ...] }
- dict_deps = {}
- for dep in dependencies:
- if dep[0] not in dict_deps:
- dict_deps[dep[0]] = []
- dict_deps[dep[0]].append(dep[1])
- # This function return True if pkg is a dependency (direct or
- # transitive) of pkg2, dependencies being listed in the deps
- # dictionary. Returns False otherwise.
- def is_dep(pkg,pkg2,deps):
- if pkg2 in deps:
- for p in deps[pkg2]:
- if pkg == p:
- return True
- if is_dep(pkg,p,deps):
- return True
- return False
- # This function eliminates transitive dependencies; for example, given
- # these dependency chain: A->{B,C} and B->{C}, the A->{C} dependency is
- # already covered by B->{C}, so C is a transitive dependency of A, via B.
- # The functions does:
- # - for each dependency d[i] of the package pkg
- # - if d[i] is a dependency of any of the other dependencies d[j]
- # - do not keep d[i]
- # - otherwise keep d[i]
- def remove_transitive_deps(pkg,deps):
- d = deps[pkg]
- new_d = []
- for i in range(len(d)):
- keep_me = True
- for j in range(len(d)):
- if j==i:
- continue
- if is_dep(d[i],d[j],deps):
- keep_me = False
- if keep_me:
- new_d.append(d[i])
- return new_d
- # This function removes the dependency on the 'toolchain' package
- def remove_toolchain_deps(pkg,deps):
- return [p for p in deps[pkg] if not p == 'toolchain']
- # This functions trims down the dependency list of all packages.
- # It applies in sequence all the dependency-elimination methods.
- def remove_extra_deps(deps):
- for pkg in list(deps.keys()):
- if not pkg == 'all':
- deps[pkg] = remove_toolchain_deps(pkg,deps)
- for pkg in list(deps.keys()):
- if not transitive or pkg == 'all':
- deps[pkg] = remove_transitive_deps(pkg,deps)
- return deps
- dict_deps = remove_extra_deps(dict_deps)
- # Print the attributes of a node: label and fill-color
- def print_attrs(pkg):
- name = pkg_node_name(pkg)
- if pkg == 'all':
- label = 'ALL'
- else:
- label = pkg
- if pkg == 'all' or (mode == MODE_PKG and pkg == rootpkg):
- color = root_colour
- else:
- if pkg.startswith('host') \
- or pkg.startswith('toolchain') \
- or pkg.startswith('rootfs'):
- color = host_colour
- else:
- color = target_colour
- print("%s [label = \"%s\"]" % (name, label))
- print("%s [color=%s,style=filled]" % (name, color))
- # Print the dependency graph of a package
- def print_pkg_deps(depth, pkg):
- if pkg in done_deps:
- return
- done_deps.append(pkg)
- print_attrs(pkg)
- if pkg not in dict_deps:
- return
- if max_depth == 0 or depth < max_depth:
- for d in dict_deps[pkg]:
- print("%s -> %s" % (pkg_node_name(pkg), pkg_node_name(d)))
- print_pkg_deps(depth+1, d)
- # Start printing the graph data
- print("digraph G {")
- done_deps = []
- print_pkg_deps(0, rootpkg)
- print("}")
|