xref: /OK3568_Linux_fs/buildroot/support/scripts/graph-depends (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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