2 # Copyright 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Dumps a graph of allowed and disallowed inter-module dependencies described
7 by the DEPS files in the source tree. Supports DOT and PNG as the output format.
9 Enables filtering and differential highlighting of parts of the graph based on
10 the specified criteria. This allows for a much easier visual analysis of the
11 dependencies, including answering questions such as "if a new source must
12 depend on modules A, B, and C, what valid options among the existing modules
13 are there to put it in."
15 See README.md for a detailed description of the DEPS format.
24 from builddeps import DepsBuilder
25 from rules import Rule
28 class DepsGrapher(DepsBuilder):
29 """Parses include_rules from DEPS files and outputs a DOT graph of the
30 allowed and disallowed dependencies between directories and specific file
31 regexps. Can generate only a subgraph of the whole dependency graph
32 corresponding to the provided inclusion and exclusion regexp filters.
33 Also can highlight fanins and/or fanouts of certain nodes matching the
34 provided regexp patterns.
43 ignore_specific_rules,
53 """Creates a new DepsGrapher.
56 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
57 verbose: Set to true for debug output.
58 being_tested: Set to true to ignore the DEPS file at tools/graphdeps/DEPS.
59 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
60 ignore_specific_rules: Ignore rules from specific_include_rules sections.
61 hide_disallowed_deps: Hide disallowed dependencies from the output graph.
62 out_file: Output file name.
63 out_format: Output format (anything GraphViz dot's -T option supports).
64 layout_engine: Layout engine for formats other than 'dot'
65 (anything that GraphViz dot's -K option supports).
66 unflatten_graph: Try to reformat the output graph so it is narrower and
67 taller. Helps fight overly flat and wide graphs, but
68 sometimes produces a worse result.
69 incl: Include only nodes matching this regexp; such nodes' fanin/fanout
71 excl: Exclude nodes matching this regexp; such nodes' fanin/fanout is
72 processed independently.
73 hilite_fanins: Highlight fanins of nodes matching this regexp with a
74 different edge and node color.
75 hilite_fanouts: Highlight fanouts of nodes matching this regexp with a
76 different edge and node color.
85 ignore_specific_rules)
87 self.ignore_temp_rules = ignore_temp_rules
88 self.ignore_specific_rules = ignore_specific_rules
89 self.hide_disallowed_deps = hide_disallowed_deps
90 self.out_file = out_file
91 self.out_format = out_format
92 self.layout_engine = layout_engine
93 self.unflatten_graph = unflatten_graph
96 self.hilite_fanins = hilite_fanins
97 self.hilite_fanouts = hilite_fanouts
101 def DumpDependencies(self):
102 """ Builds a dependency rule table and dumps the corresponding dependency
103 graph to all requested formats."""
104 self._BuildDepsGraph()
105 self._DumpDependencies()
107 def _BuildDepsGraph(self):
108 """Recursively traverses the source tree starting at the specified directory
109 and builds a dependency graph representation in self.deps."""
110 for (rules, _) in self.GetAllRulesAndFiles():
111 deps = rules.AsDependencyTuples(
112 include_general_rules=True,
113 include_specific_rules=not self.ignore_specific_rules)
114 self.deps.update(deps)
116 def _DumpDependencies(self):
117 """Dumps the built dependency graph to the specified file with specified
119 if self.out_format == 'dot' and not self.layout_engine:
120 if self.unflatten_graph:
121 pipe = pipes.Template()
122 pipe.append('unflatten -l 2 -c 3', '--')
123 out = pipe.open(self.out_file, 'w')
125 out = open(self.out_file, 'w')
127 pipe = pipes.Template()
128 if self.unflatten_graph:
129 pipe.append('unflatten -l 2 -c 3', '--')
130 dot_cmd = 'dot -T' + self.out_format
131 if self.layout_engine:
132 dot_cmd += ' -K' + self.layout_engine
133 pipe.append(dot_cmd, '--')
134 out = pipe.open(self.out_file, 'w')
136 self._DumpDependenciesImpl(self.deps, out)
139 def _DumpDependenciesImpl(self, deps, out):
140 """Computes nodes' and edges' properties for the dependency graph |deps| and
141 carries out the actual dumping to a file/pipe |out|."""
145 # Pre-initialize the graph with src->(dst, allow) pairs.
146 for (allow, src, dst) in deps:
147 if allow == Rule.TEMP_ALLOW and self.ignore_temp_rules:
151 if src not in deps_graph:
153 deps_graph[src].append((dst, allow))
155 # Add all hierarchical parents too, in case some of them don't have their
156 # own DEPS, and therefore are missing from the list of rules. Those will
157 # be recursively populated with their parents' rules in the next block.
158 parent_src = os.path.dirname(src)
160 if parent_src not in deps_graph:
161 deps_graph[parent_src] = []
162 parent_src = os.path.dirname(parent_src)
164 # For every node, propagate its rules down to all its children.
165 deps_srcs = list(deps_srcs)
167 for src in deps_srcs:
168 parent_src = os.path.dirname(src)
170 # We presort the list, so parents are guaranteed to precede children.
171 assert parent_src in deps_graph,\
172 "src: %s, parent_src: %s" % (src, parent_src)
173 for (dst, allow) in deps_graph[parent_src]:
174 # Check that this node does not explicitly override a rule from the
175 # parent that we're about to add.
176 if ((dst, Rule.ALLOW) not in deps_graph[src]) and \
177 ((dst, Rule.TEMP_ALLOW) not in deps_graph[src]) and \
178 ((dst, Rule.DISALLOW) not in deps_graph[src]):
179 deps_graph[src].append((dst, allow))
184 # 1) Populate a list of edge specifications in DOT format;
185 # 2) Populate a list of computed raw node attributes to be output as node
186 # specifications in DOT format later on.
187 # Edges and nodes are emphasized with color and line/border weight depending
188 # on how many of incl/excl/hilite_fanins/hilite_fanouts filters they hit,
190 for src in deps_graph.keys():
191 for (dst, allow) in deps_graph[src]:
192 if allow == Rule.DISALLOW and self.hide_disallowed_deps:
195 if allow == Rule.ALLOW and src == dst:
198 edge_spec = "%s->%s" % (src, dst)
199 if not re.search(self.incl, edge_spec) or \
200 re.search(self.excl, edge_spec):
203 if src not in node_props:
204 node_props[src] = {'hilite': None, 'degree': 0}
205 if dst not in node_props:
206 node_props[dst] = {'hilite': None, 'degree': 0}
210 if self.hilite_fanouts and re.search(self.hilite_fanouts, src):
211 node_props[src]['hilite'] = 'lightgreen'
212 node_props[dst]['hilite'] = 'lightblue'
213 node_props[dst]['degree'] += 1
216 if self.hilite_fanins and re.search(self.hilite_fanins, dst):
217 node_props[src]['hilite'] = 'lightblue'
218 node_props[dst]['hilite'] = 'lightgreen'
219 node_props[src]['degree'] += 1
222 if allow == Rule.ALLOW:
223 edge_color = (edge_weight > 1) and 'blue' or 'green'
225 elif allow == Rule.TEMP_ALLOW:
226 edge_color = (edge_weight > 1) and 'blue' or 'green'
227 edge_style = 'dashed'
230 edge_style = 'dashed'
231 edges.append(' "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \
232 (src, dst, edge_style, edge_color, edge_weight))
234 # Reformat the computed raw node attributes into a final DOT representation.
236 for (node, attrs) in node_props.iteritems():
239 attr_strs.append('style=filled,fillcolor=%s' % attrs['hilite'])
240 attr_strs.append('penwidth=%d' % (attrs['degree'] or 1))
241 nodes.append(' "%s" [%s];' % (node, ','.join(attr_strs)))
243 # Output nodes and edges to |out| (can be a file or a pipe).
246 out.write('digraph DEPS {\n'
248 out.write('\n'.join(nodes))
250 out.write('\n'.join(edges))
256 print """Usage: python graphdeps.py [--root <root>]
258 --root ROOT Specifies the repository root. This defaults to "../../.."
259 relative to the script file. This will be correct given the
260 normal location of the script in "<root>/tools/graphdeps".
262 --(others) There are a few lesser-used options; run with --help to show them.
265 Dump the whole dependency graph:
267 Find a suitable place for a new source that must depend on /apps and
268 /content/browser/renderer_host. Limit potential candidates to /apps,
269 /chrome/browser and content/browser, and descendants of those three.
270 Generate both DOT and PNG output. The output will highlight the fanins
271 of /apps and /content/browser/renderer_host. Overlapping nodes in both fanins
272 will be emphasized by a thicker border. Those nodes are the ones that are
273 allowed to depend on both targets, therefore they are all legal candidates
274 to place the new source in:
279 --incl='^(apps|chrome/browser|content/browser)->.*' \
280 --excl='.*->third_party' \
281 --fanin='^(apps|content/browser/renderer_host)$' \
282 --ignore-specific-rules \
283 --ignore-temp-rules"""
287 option_parser = optparse.OptionParser()
288 option_parser.add_option(
290 default="", dest="base_directory",
291 help="Specifies the repository root. This defaults "
292 "to '../../..' relative to the script file, which "
293 "will normally be the repository root.")
294 option_parser.add_option(
296 action='append', dest='extra_repos', default=[],
297 help='Specifies extra repositories relative to root repository.')
298 option_parser.add_option(
300 dest="out_format", default="dot",
301 help="Output file format. "
302 "Can be anything that GraphViz dot's -T option supports. "
303 "The most useful ones are: dot (text), svg (image), pdf (image)."
304 "NOTES: dotty has a known problem with fonts when displaying DOT "
305 "files on Ubuntu - if labels are unreadable, try other formats.")
306 option_parser.add_option(
308 dest="out_file", default="DEPS",
309 help="Output file name. If the name does not end in an extension "
310 "matching the output format, that extension is automatically "
312 option_parser.add_option(
313 "-l", "--layout-engine",
314 dest="layout_engine", default="",
315 help="Layout rendering engine. "
316 "Can be anything that GraphViz dot's -K option supports. "
317 "The most useful are in decreasing order: dot, fdp, circo, osage. "
318 "NOTE: '-f dot' and '-f dot -l dot' are different: the former "
319 "will dump a raw DOT graph and stop; the latter will further "
320 "filter it through 'dot -Tdot -Kdot' layout engine.")
321 option_parser.add_option(
323 default="^.*$", dest="incl",
324 help="Include only edges of the graph that match the specified regexp. "
325 "The regexp is applied to edges of the graph formatted as "
326 "'source_node->target_node', where the '->' part is vebatim. "
327 "Therefore, a reliable regexp should look like "
328 "'^(chrome|chrome/browser|chrome/common)->content/public/browser$' "
329 "or similar, with both source and target node regexps present, "
330 "explicit ^ and $, and otherwise being as specific as possible.")
331 option_parser.add_option(
333 default="^$", dest="excl",
334 help="Exclude dependent nodes that match the specified regexp. "
335 "See --incl for details on the format.")
336 option_parser.add_option(
338 default="", dest="hilite_fanins",
339 help="Highlight fanins of nodes matching the specified regexp.")
340 option_parser.add_option(
342 default="", dest="hilite_fanouts",
343 help="Highlight fanouts of nodes matching the specified regexp.")
344 option_parser.add_option(
345 "", "--ignore-temp-rules",
346 action="store_true", dest="ignore_temp_rules", default=False,
347 help="Ignore !-prefixed (temporary) rules in DEPS files.")
348 option_parser.add_option(
349 "", "--ignore-specific-rules",
350 action="store_true", dest="ignore_specific_rules", default=False,
351 help="Ignore specific_include_rules section of DEPS files.")
352 option_parser.add_option(
353 "", "--hide-disallowed-deps",
354 action="store_true", dest="hide_disallowed_deps", default=False,
355 help="Hide disallowed dependencies in the output graph.")
356 option_parser.add_option(
358 action="store_true", dest="unflatten_graph", default=False,
359 help="Try to reformat the output graph so it is narrower and taller. "
360 "Helps fight overly flat and wide graphs, but sometimes produces "
362 option_parser.add_option(
364 action="store_true", default=False,
365 help="Print debug logging")
366 options, args = option_parser.parse_args()
368 if not options.out_file.endswith(options.out_format):
369 options.out_file += '.' + options.out_format
371 deps_grapher = DepsGrapher(
372 base_directory=options.base_directory,
373 extra_repos=options.extra_repos,
374 verbose=options.verbose,
377 ignore_temp_rules=options.ignore_temp_rules,
378 ignore_specific_rules=options.ignore_specific_rules,
379 hide_disallowed_deps=options.hide_disallowed_deps,
381 out_file=options.out_file,
382 out_format=options.out_format,
383 layout_engine=options.layout_engine,
384 unflatten_graph=options.unflatten_graph,
388 hilite_fanins=options.hilite_fanins,
389 hilite_fanouts=options.hilite_fanouts)
395 print 'Using base directory: ', deps_grapher.base_directory
396 print 'include nodes : ', options.incl
397 print 'exclude nodes : ', options.excl
398 print 'highlight fanins of : ', options.hilite_fanins
399 print 'highlight fanouts of: ', options.hilite_fanouts
401 deps_grapher.DumpDependencies()
405 if '__main__' == __name__: