|  | #!/usr/bin/env python | 
|  | # Copyright 2013 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """Dumps a graph of allowed and disallowed inter-module dependencies described | 
|  | by the DEPS files in the source tree. Supports DOT and PNG as the output format. | 
|  |  | 
|  | Enables filtering and differential highlighting of parts of the graph based on | 
|  | the specified criteria. This allows for a much easier visual analysis of the | 
|  | dependencies, including answering questions such as "if a new source must | 
|  | depend on modules A, B, and C, what valid options among the existing modules | 
|  | are there to put it in." | 
|  |  | 
|  | See README.md for a detailed description of the DEPS format. | 
|  | """ | 
|  |  | 
|  | import os | 
|  | import optparse | 
|  | import pipes | 
|  | import re | 
|  | import sys | 
|  |  | 
|  | from builddeps import DepsBuilder | 
|  | from rules import Rule | 
|  |  | 
|  |  | 
|  | class DepsGrapher(DepsBuilder): | 
|  | """Parses include_rules from DEPS files and outputs a DOT graph of the | 
|  | allowed and disallowed dependencies between directories and specific file | 
|  | regexps. Can generate only a subgraph of the whole dependency graph | 
|  | corresponding to the provided inclusion and exclusion regexp filters. | 
|  | Also can highlight fanins and/or fanouts of certain nodes matching the | 
|  | provided regexp patterns. | 
|  | """ | 
|  |  | 
|  | def __init__(self, | 
|  | base_directory, | 
|  | extra_repos, | 
|  | verbose, | 
|  | being_tested, | 
|  | ignore_temp_rules, | 
|  | ignore_specific_rules, | 
|  | hide_disallowed_deps, | 
|  | out_file, | 
|  | out_format, | 
|  | layout_engine, | 
|  | unflatten_graph, | 
|  | incl, | 
|  | excl, | 
|  | hilite_fanins, | 
|  | hilite_fanouts): | 
|  | """Creates a new DepsGrapher. | 
|  |  | 
|  | Args: | 
|  | base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src. | 
|  | verbose: Set to true for debug output. | 
|  | being_tested: Set to true to ignore the DEPS file at tools/graphdeps/DEPS. | 
|  | ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!"). | 
|  | ignore_specific_rules: Ignore rules from specific_include_rules sections. | 
|  | hide_disallowed_deps: Hide disallowed dependencies from the output graph. | 
|  | out_file: Output file name. | 
|  | out_format: Output format (anything GraphViz dot's -T option supports). | 
|  | layout_engine: Layout engine for formats other than 'dot' | 
|  | (anything that GraphViz dot's -K option supports). | 
|  | unflatten_graph: Try to reformat the output graph so it is narrower and | 
|  | taller. Helps fight overly flat and wide graphs, but | 
|  | sometimes produces a worse result. | 
|  | incl: Include only nodes matching this regexp; such nodes' fanin/fanout | 
|  | is also included. | 
|  | excl: Exclude nodes matching this regexp; such nodes' fanin/fanout is | 
|  | processed independently. | 
|  | hilite_fanins: Highlight fanins of nodes matching this regexp with a | 
|  | different edge and node color. | 
|  | hilite_fanouts: Highlight fanouts of nodes matching this regexp with a | 
|  | different edge and node color. | 
|  | """ | 
|  | DepsBuilder.__init__( | 
|  | self, | 
|  | base_directory, | 
|  | extra_repos, | 
|  | verbose, | 
|  | being_tested, | 
|  | ignore_temp_rules, | 
|  | ignore_specific_rules) | 
|  |  | 
|  | self.ignore_temp_rules = ignore_temp_rules | 
|  | self.ignore_specific_rules = ignore_specific_rules | 
|  | self.hide_disallowed_deps = hide_disallowed_deps | 
|  | self.out_file = out_file | 
|  | self.out_format = out_format | 
|  | self.layout_engine = layout_engine | 
|  | self.unflatten_graph = unflatten_graph | 
|  | self.incl = incl | 
|  | self.excl = excl | 
|  | self.hilite_fanins = hilite_fanins | 
|  | self.hilite_fanouts = hilite_fanouts | 
|  |  | 
|  | self.deps = set() | 
|  |  | 
|  | def DumpDependencies(self): | 
|  | """ Builds a dependency rule table and dumps the corresponding dependency | 
|  | graph to all requested formats.""" | 
|  | self._BuildDepsGraph() | 
|  | self._DumpDependencies() | 
|  |  | 
|  | def _BuildDepsGraph(self): | 
|  | """Recursively traverses the source tree starting at the specified directory | 
|  | and builds a dependency graph representation in self.deps.""" | 
|  | for (rules, _) in self.GetAllRulesAndFiles(): | 
|  | deps = rules.AsDependencyTuples( | 
|  | include_general_rules=True, | 
|  | include_specific_rules=not self.ignore_specific_rules) | 
|  | self.deps.update(deps) | 
|  |  | 
|  | def _DumpDependencies(self): | 
|  | """Dumps the built dependency graph to the specified file with specified | 
|  | format.""" | 
|  | if self.out_format == 'dot' and not self.layout_engine: | 
|  | if self.unflatten_graph: | 
|  | pipe = pipes.Template() | 
|  | pipe.append('unflatten -l 2 -c 3', '--') | 
|  | out = pipe.open(self.out_file, 'w') | 
|  | else: | 
|  | out = open(self.out_file, 'w') | 
|  | else: | 
|  | pipe = pipes.Template() | 
|  | if self.unflatten_graph: | 
|  | pipe.append('unflatten -l 2 -c 3', '--') | 
|  | dot_cmd = 'dot -T' + self.out_format | 
|  | if self.layout_engine: | 
|  | dot_cmd += ' -K' + self.layout_engine | 
|  | pipe.append(dot_cmd, '--') | 
|  | out = pipe.open(self.out_file, 'w') | 
|  |  | 
|  | self._DumpDependenciesImpl(self.deps, out) | 
|  | out.close() | 
|  |  | 
|  | def _DumpDependenciesImpl(self, deps, out): | 
|  | """Computes nodes' and edges' properties for the dependency graph |deps| and | 
|  | carries out the actual dumping to a file/pipe |out|.""" | 
|  | deps_graph = dict() | 
|  | deps_srcs = set() | 
|  |  | 
|  | # Pre-initialize the graph with src->(dst, allow) pairs. | 
|  | for (allow, src, dst) in deps: | 
|  | if allow == Rule.TEMP_ALLOW and self.ignore_temp_rules: | 
|  | continue | 
|  |  | 
|  | deps_srcs.add(src) | 
|  | if src not in deps_graph: | 
|  | deps_graph[src] = [] | 
|  | deps_graph[src].append((dst, allow)) | 
|  |  | 
|  | # Add all hierarchical parents too, in case some of them don't have their | 
|  | # own DEPS, and therefore are missing from the list of rules. Those will | 
|  | # be recursively populated with their parents' rules in the next block. | 
|  | parent_src = os.path.dirname(src) | 
|  | while parent_src: | 
|  | if parent_src not in deps_graph: | 
|  | deps_graph[parent_src] = [] | 
|  | parent_src = os.path.dirname(parent_src) | 
|  |  | 
|  | # For every node, propagate its rules down to all its children. | 
|  | deps_srcs = list(deps_srcs) | 
|  | deps_srcs.sort() | 
|  | for src in deps_srcs: | 
|  | parent_src = os.path.dirname(src) | 
|  | if parent_src: | 
|  | # We presort the list, so parents are guaranteed to precede children. | 
|  | assert parent_src in deps_graph,\ | 
|  | "src: %s, parent_src: %s" % (src, parent_src) | 
|  | for (dst, allow) in deps_graph[parent_src]: | 
|  | # Check that this node does not explicitly override a rule from the | 
|  | # parent that we're about to add. | 
|  | if ((dst, Rule.ALLOW) not in deps_graph[src]) and \ | 
|  | ((dst, Rule.TEMP_ALLOW) not in deps_graph[src]) and \ | 
|  | ((dst, Rule.DISALLOW) not in deps_graph[src]): | 
|  | deps_graph[src].append((dst, allow)) | 
|  |  | 
|  | node_props = {} | 
|  | edges = [] | 
|  |  | 
|  | # 1) Populate a list of edge specifications in DOT format; | 
|  | # 2) Populate a list of computed raw node attributes to be output as node | 
|  | #    specifications in DOT format later on. | 
|  | # Edges and nodes are emphasized with color and line/border weight depending | 
|  | # on how many of incl/excl/hilite_fanins/hilite_fanouts filters they hit, | 
|  | # and in what way. | 
|  | for src in deps_graph.keys(): | 
|  | for (dst, allow) in deps_graph[src]: | 
|  | if allow == Rule.DISALLOW and self.hide_disallowed_deps: | 
|  | continue | 
|  |  | 
|  | if allow == Rule.ALLOW and src == dst: | 
|  | continue | 
|  |  | 
|  | edge_spec = "%s->%s" % (src, dst) | 
|  | if not re.search(self.incl, edge_spec) or \ | 
|  | re.search(self.excl, edge_spec): | 
|  | continue | 
|  |  | 
|  | if src not in node_props: | 
|  | node_props[src] = {'hilite': None, 'degree': 0} | 
|  | if dst not in node_props: | 
|  | node_props[dst] = {'hilite': None, 'degree': 0} | 
|  |  | 
|  | edge_weight = 1 | 
|  |  | 
|  | if self.hilite_fanouts and re.search(self.hilite_fanouts, src): | 
|  | node_props[src]['hilite'] = 'lightgreen' | 
|  | node_props[dst]['hilite'] = 'lightblue' | 
|  | node_props[dst]['degree'] += 1 | 
|  | edge_weight += 1 | 
|  |  | 
|  | if self.hilite_fanins and re.search(self.hilite_fanins, dst): | 
|  | node_props[src]['hilite'] = 'lightblue' | 
|  | node_props[dst]['hilite'] = 'lightgreen' | 
|  | node_props[src]['degree'] += 1 | 
|  | edge_weight += 1 | 
|  |  | 
|  | if allow == Rule.ALLOW: | 
|  | edge_color = (edge_weight > 1) and 'blue' or 'green' | 
|  | edge_style = 'solid' | 
|  | elif allow == Rule.TEMP_ALLOW: | 
|  | edge_color = (edge_weight > 1) and 'blue' or 'green' | 
|  | edge_style = 'dashed' | 
|  | else: | 
|  | edge_color = 'red' | 
|  | edge_style = 'dashed' | 
|  | edges.append('    "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \ | 
|  | (src, dst, edge_style, edge_color, edge_weight)) | 
|  |  | 
|  | # Reformat the computed raw node attributes into a final DOT representation. | 
|  | nodes = [] | 
|  | for (node, attrs) in node_props.iteritems(): | 
|  | attr_strs = [] | 
|  | if attrs['hilite']: | 
|  | attr_strs.append('style=filled,fillcolor=%s' % attrs['hilite']) | 
|  | attr_strs.append('penwidth=%d' % (attrs['degree'] or 1)) | 
|  | nodes.append('    "%s" [%s];' % (node, ','.join(attr_strs))) | 
|  |  | 
|  | # Output nodes and edges to |out| (can be a file or a pipe). | 
|  | edges.sort() | 
|  | nodes.sort() | 
|  | out.write('digraph DEPS {\n' | 
|  | '    fontsize=8;\n') | 
|  | out.write('\n'.join(nodes)) | 
|  | out.write('\n\n') | 
|  | out.write('\n'.join(edges)) | 
|  | out.write('\n}\n') | 
|  | out.close() | 
|  |  | 
|  |  | 
|  | def PrintUsage(): | 
|  | print """Usage: python graphdeps.py [--root <root>] | 
|  |  | 
|  | --root ROOT Specifies the repository root. This defaults to "../../.." | 
|  | relative to the script file. This will be correct given the | 
|  | normal location of the script in "<root>/tools/graphdeps". | 
|  |  | 
|  | --(others)  There are a few lesser-used options; run with --help to show them. | 
|  |  | 
|  | Examples: | 
|  | Dump the whole dependency graph: | 
|  | graphdeps.py | 
|  | Find a suitable place for a new source that must depend on /apps and | 
|  | /content/browser/renderer_host. Limit potential candidates to /apps, | 
|  | /chrome/browser and content/browser, and descendants of those three. | 
|  | Generate both DOT and PNG output. The output will highlight the fanins | 
|  | of /apps and /content/browser/renderer_host. Overlapping nodes in both fanins | 
|  | will be emphasized by a thicker border. Those nodes are the ones that are | 
|  | allowed to depend on both targets, therefore they are all legal candidates | 
|  | to place the new source in: | 
|  | graphdeps.py \ | 
|  | --root=./src \ | 
|  | --out=./DEPS.svg \ | 
|  | --format=svg \ | 
|  | --incl='^(apps|chrome/browser|content/browser)->.*' \ | 
|  | --excl='.*->third_party' \ | 
|  | --fanin='^(apps|content/browser/renderer_host)$' \ | 
|  | --ignore-specific-rules \ | 
|  | --ignore-temp-rules""" | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | option_parser = optparse.OptionParser() | 
|  | option_parser.add_option( | 
|  | "", "--root", | 
|  | default="", dest="base_directory", | 
|  | help="Specifies the repository root. This defaults " | 
|  | "to '../../..' relative to the script file, which " | 
|  | "will normally be the repository root.") | 
|  | option_parser.add_option( | 
|  | '', '--extra-repos', | 
|  | action='append', dest='extra_repos', default=[], | 
|  | help='Specifies extra repositories relative to root repository.') | 
|  | option_parser.add_option( | 
|  | "-f", "--format", | 
|  | dest="out_format", default="dot", | 
|  | help="Output file format. " | 
|  | "Can be anything that GraphViz dot's -T option supports. " | 
|  | "The most useful ones are: dot (text), svg (image), pdf (image)." | 
|  | "NOTES: dotty has a known problem with fonts when displaying DOT " | 
|  | "files on Ubuntu - if labels are unreadable, try other formats.") | 
|  | option_parser.add_option( | 
|  | "-o", "--out", | 
|  | dest="out_file", default="DEPS", | 
|  | help="Output file name. If the name does not end in an extension " | 
|  | "matching the output format, that extension is automatically " | 
|  | "appended.") | 
|  | option_parser.add_option( | 
|  | "-l", "--layout-engine", | 
|  | dest="layout_engine", default="", | 
|  | help="Layout rendering engine. " | 
|  | "Can be anything that GraphViz dot's -K option supports. " | 
|  | "The most useful are in decreasing order: dot, fdp, circo, osage. " | 
|  | "NOTE: '-f dot' and '-f dot -l dot' are different: the former " | 
|  | "will dump a raw DOT graph and stop; the latter will further " | 
|  | "filter it through 'dot -Tdot -Kdot' layout engine.") | 
|  | option_parser.add_option( | 
|  | "-i", "--incl", | 
|  | default="^.*$", dest="incl", | 
|  | help="Include only edges of the graph that match the specified regexp. " | 
|  | "The regexp is applied to edges of the graph formatted as " | 
|  | "'source_node->target_node', where the '->' part is vebatim. " | 
|  | "Therefore, a reliable regexp should look like " | 
|  | "'^(chrome|chrome/browser|chrome/common)->content/public/browser$' " | 
|  | "or similar, with both source and target node regexps present, " | 
|  | "explicit ^ and $, and otherwise being as specific as possible.") | 
|  | option_parser.add_option( | 
|  | "-e", "--excl", | 
|  | default="^$", dest="excl", | 
|  | help="Exclude dependent nodes that match the specified regexp. " | 
|  | "See --incl for details on the format.") | 
|  | option_parser.add_option( | 
|  | "", "--fanin", | 
|  | default="", dest="hilite_fanins", | 
|  | help="Highlight fanins of nodes matching the specified regexp.") | 
|  | option_parser.add_option( | 
|  | "", "--fanout", | 
|  | default="", dest="hilite_fanouts", | 
|  | help="Highlight fanouts of nodes matching the specified regexp.") | 
|  | option_parser.add_option( | 
|  | "", "--ignore-temp-rules", | 
|  | action="store_true", dest="ignore_temp_rules", default=False, | 
|  | help="Ignore !-prefixed (temporary) rules in DEPS files.") | 
|  | option_parser.add_option( | 
|  | "", "--ignore-specific-rules", | 
|  | action="store_true", dest="ignore_specific_rules", default=False, | 
|  | help="Ignore specific_include_rules section of DEPS files.") | 
|  | option_parser.add_option( | 
|  | "", "--hide-disallowed-deps", | 
|  | action="store_true", dest="hide_disallowed_deps", default=False, | 
|  | help="Hide disallowed dependencies in the output graph.") | 
|  | option_parser.add_option( | 
|  | "", "--unflatten", | 
|  | action="store_true", dest="unflatten_graph", default=False, | 
|  | help="Try to reformat the output graph so it is narrower and taller. " | 
|  | "Helps fight overly flat and wide graphs, but sometimes produces " | 
|  | "inferior results.") | 
|  | option_parser.add_option( | 
|  | "-v", "--verbose", | 
|  | action="store_true", default=False, | 
|  | help="Print debug logging") | 
|  | options, args = option_parser.parse_args() | 
|  |  | 
|  | if not options.out_file.endswith(options.out_format): | 
|  | options.out_file += '.' + options.out_format | 
|  |  | 
|  | deps_grapher = DepsGrapher( | 
|  | base_directory=options.base_directory, | 
|  | extra_repos=options.extra_repos, | 
|  | verbose=options.verbose, | 
|  | being_tested=False, | 
|  |  | 
|  | ignore_temp_rules=options.ignore_temp_rules, | 
|  | ignore_specific_rules=options.ignore_specific_rules, | 
|  | hide_disallowed_deps=options.hide_disallowed_deps, | 
|  |  | 
|  | out_file=options.out_file, | 
|  | out_format=options.out_format, | 
|  | layout_engine=options.layout_engine, | 
|  | unflatten_graph=options.unflatten_graph, | 
|  |  | 
|  | incl=options.incl, | 
|  | excl=options.excl, | 
|  | hilite_fanins=options.hilite_fanins, | 
|  | hilite_fanouts=options.hilite_fanouts) | 
|  |  | 
|  | if len(args) > 0: | 
|  | PrintUsage() | 
|  | return 1 | 
|  |  | 
|  | print 'Using base directory: ', deps_grapher.base_directory | 
|  | print 'include nodes       : ', options.incl | 
|  | print 'exclude nodes       : ', options.excl | 
|  | print 'highlight fanins of : ', options.hilite_fanins | 
|  | print 'highlight fanouts of: ', options.hilite_fanouts | 
|  |  | 
|  | deps_grapher.DumpDependencies() | 
|  | return 0 | 
|  |  | 
|  |  | 
|  | if '__main__' == __name__: | 
|  | sys.exit(main()) |