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 builddeps.py 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.
42 ignore_specific_rules
,
52 """Creates a new DepsGrapher.
55 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src.
56 verbose: Set to true for debug output.
57 being_tested: Set to true to ignore the DEPS file at tools/graphdeps/DEPS.
58 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
59 ignore_specific_rules: Ignore rules from specific_include_rules sections.
60 hide_disallowed_deps: Hide disallowed dependencies from the output graph.
61 out_file: Output file name.
62 out_format: Output format (anything GraphViz dot's -T option supports).
63 layout_engine: Layout engine for formats other than 'dot'
64 (anything that GraphViz dot's -K option supports).
65 unflatten_graph: Try to reformat the output graph so it is narrower and
66 taller. Helps fight overly flat and wide graphs, but
67 sometimes produces a worse result.
68 incl: Include only nodes matching this regexp; such nodes' fanin/fanout
70 excl: Exclude nodes matching this regexp; such nodes' fanin/fanout is
71 processed independently.
72 hilite_fanins: Highlight fanins of nodes matching this regexp with a
73 different edge and node color.
74 hilite_fanouts: Highlight fanouts of nodes matching this regexp with a
75 different edge and node color.
83 ignore_specific_rules
)
85 self
.ignore_temp_rules
= ignore_temp_rules
86 self
.ignore_specific_rules
= ignore_specific_rules
87 self
.hide_disallowed_deps
= hide_disallowed_deps
88 self
.out_file
= out_file
89 self
.out_format
= out_format
90 self
.layout_engine
= layout_engine
91 self
.unflatten_graph
= unflatten_graph
94 self
.hilite_fanins
= hilite_fanins
95 self
.hilite_fanouts
= hilite_fanouts
99 def DumpDependencies(self
):
100 """ Builds a dependency rule table and dumps the corresponding dependency
101 graph to all requested formats."""
102 self
._BuildDepsGraph
(self
.base_directory
)
103 self
._DumpDependencies
()
105 def _BuildDepsGraph(self
, full_path
):
106 """Recursively traverses the source tree starting at the specified directory
107 and builds a dependency graph representation in self.deps."""
108 rel_path
= os
.path
.relpath(full_path
, self
.base_directory
)
109 #if re.search(self.incl, rel_path) and not re.search(self.excl, rel_path):
110 rules
= self
.GetDirectoryRules(full_path
)
112 deps
= rules
.AsDependencyTuples(
113 include_general_rules
=True,
114 include_specific_rules
=not self
.ignore_specific_rules
)
115 self
.deps
.update(deps
)
117 for item
in os
.listdir(full_path
):
118 next_full_path
= os
.path
.join(full_path
, item
)
119 if os
.path
.isdir(next_full_path
):
120 self
._BuildDepsGraph
(next_full_path
)
122 def _DumpDependencies(self
):
123 """Dumps the built dependency graph to the specified file with specified
125 if self
.out_format
== 'dot' and not self
.layout_engine
:
126 if self
.unflatten_graph
:
127 pipe
= pipes
.Template()
128 pipe
.append('unflatten -l 2 -c 3', '--')
129 out
= pipe
.open(self
.out_file
, 'w')
131 out
= open(self
.out_file
, 'w')
133 pipe
= pipes
.Template()
134 if self
.unflatten_graph
:
135 pipe
.append('unflatten -l 2 -c 3', '--')
136 dot_cmd
= 'dot -T' + self
.out_format
137 if self
.layout_engine
:
138 dot_cmd
+= ' -K' + self
.layout_engine
139 pipe
.append(dot_cmd
, '--')
140 out
= pipe
.open(self
.out_file
, 'w')
142 self
._DumpDependenciesImpl
(self
.deps
, out
)
145 def _DumpDependenciesImpl(self
, deps
, out
):
146 """Computes nodes' and edges' properties for the dependency graph |deps| and
147 carries out the actual dumping to a file/pipe |out|."""
151 # Pre-initialize the graph with src->(dst, allow) pairs.
152 for (allow
, src
, dst
) in deps
:
153 if allow
== Rule
.TEMP_ALLOW
and self
.ignore_temp_rules
:
157 if src
not in deps_graph
:
159 deps_graph
[src
].append((dst
, allow
))
161 # Add all hierarchical parents too, in case some of them don't have their
162 # own DEPS, and therefore are missing from the list of rules. Those will
163 # be recursively populated with their parents' rules in the next block.
164 parent_src
= os
.path
.dirname(src
)
166 if parent_src
not in deps_graph
:
167 deps_graph
[parent_src
] = []
168 parent_src
= os
.path
.dirname(parent_src
)
170 # For every node, propagate its rules down to all its children.
171 deps_srcs
= list(deps_srcs
)
173 for src
in deps_srcs
:
174 parent_src
= os
.path
.dirname(src
)
176 # We presort the list, so parents are guaranteed to precede children.
177 assert parent_src
in deps_graph
,\
178 "src: %s, parent_src: %s" % (src
, parent_src
)
179 for (dst
, allow
) in deps_graph
[parent_src
]:
180 # Check that this node does not explicitly override a rule from the
181 # parent that we're about to add.
182 if ((dst
, Rule
.ALLOW
) not in deps_graph
[src
]) and \
183 ((dst
, Rule
.TEMP_ALLOW
) not in deps_graph
[src
]) and \
184 ((dst
, Rule
.DISALLOW
) not in deps_graph
[src
]):
185 deps_graph
[src
].append((dst
, allow
))
190 # 1) Populate a list of edge specifications in DOT format;
191 # 2) Populate a list of computed raw node attributes to be output as node
192 # specifications in DOT format later on.
193 # Edges and nodes are emphasized with color and line/border weight depending
194 # on how many of incl/excl/hilite_fanins/hilite_fanouts filters they hit,
196 for src
in deps_graph
.keys():
197 for (dst
, allow
) in deps_graph
[src
]:
198 if allow
== Rule
.DISALLOW
and self
.hide_disallowed_deps
:
201 if allow
== Rule
.ALLOW
and src
== dst
:
204 edge_spec
= "%s->%s" % (src
, dst
)
205 if not re
.search(self
.incl
, edge_spec
) or \
206 re
.search(self
.excl
, edge_spec
):
209 if src
not in node_props
:
210 node_props
[src
] = {'hilite': None, 'degree': 0}
211 if dst
not in node_props
:
212 node_props
[dst
] = {'hilite': None, 'degree': 0}
216 if self
.hilite_fanouts
and re
.search(self
.hilite_fanouts
, src
):
217 node_props
[src
]['hilite'] = 'lightgreen'
218 node_props
[dst
]['hilite'] = 'lightblue'
219 node_props
[dst
]['degree'] += 1
222 if self
.hilite_fanins
and re
.search(self
.hilite_fanins
, dst
):
223 node_props
[src
]['hilite'] = 'lightblue'
224 node_props
[dst
]['hilite'] = 'lightgreen'
225 node_props
[src
]['degree'] += 1
228 if allow
== Rule
.ALLOW
:
229 edge_color
= (edge_weight
> 1) and 'blue' or 'green'
231 elif allow
== Rule
.TEMP_ALLOW
:
232 edge_color
= (edge_weight
> 1) and 'blue' or 'green'
233 edge_style
= 'dashed'
236 edge_style
= 'dashed'
237 edges
.append(' "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \
238 (src
, dst
, edge_style
, edge_color
, edge_weight
))
240 # Reformat the computed raw node attributes into a final DOT representation.
242 for (node
, attrs
) in node_props
.iteritems():
245 attr_strs
.append('style=filled,fillcolor=%s' % attrs
['hilite'])
246 attr_strs
.append('penwidth=%d' % (attrs
['degree'] or 1))
247 nodes
.append(' "%s" [%s];' % (node
, ','.join(attr_strs
)))
249 # Output nodes and edges to |out| (can be a file or a pipe).
252 out
.write('digraph DEPS {\n'
254 out
.write('\n'.join(nodes
))
256 out
.write('\n'.join(edges
))
262 print """Usage: python graphdeps.py [--root <root>]
264 --root ROOT Specifies the repository root. This defaults to "../../.."
265 relative to the script file. This will be correct given the
266 normal location of the script in "<root>/tools/graphdeps".
268 --(others) There are a few lesser-used options; run with --help to show them.
271 Dump the whole dependency graph:
273 Find a suitable place for a new source that must depend on /apps and
274 /content/browser/renderer_host. Limit potential candidates to /apps,
275 /chrome/browser and content/browser, and descendants of those three.
276 Generate both DOT and PNG output. The output will highlight the fanins
277 of /apps and /content/browser/renderer_host. Overlapping nodes in both fanins
278 will be emphasized by a thicker border. Those nodes are the ones that are
279 allowed to depend on both targets, therefore they are all legal candidates
280 to place the new source in:
285 --incl='^(apps|chrome/browser|content/browser)->.*' \
286 --excl='.*->third_party' \
287 --fanin='^(apps|content/browser/renderer_host)$' \
288 --ignore-specific-rules \
289 --ignore-temp-rules"""
293 option_parser
= optparse
.OptionParser()
294 option_parser
.add_option(
296 default
="", dest
="base_directory",
297 help="Specifies the repository root. This defaults "
298 "to '../../..' relative to the script file, which "
299 "will normally be the repository root.")
300 option_parser
.add_option(
302 dest
="out_format", default
="dot",
303 help="Output file format. "
304 "Can be anything that GraphViz dot's -T option supports. "
305 "The most useful ones are: dot (text), svg (image), pdf (image)."
306 "NOTES: dotty has a known problem with fonts when displaying DOT "
307 "files on Ubuntu - if labels are unreadable, try other formats.")
308 option_parser
.add_option(
310 dest
="out_file", default
="DEPS",
311 help="Output file name. If the name does not end in an extension "
312 "matching the output format, that extension is automatically "
314 option_parser
.add_option(
315 "-l", "--layout-engine",
316 dest
="layout_engine", default
="",
317 help="Layout rendering engine. "
318 "Can be anything that GraphViz dot's -K option supports. "
319 "The most useful are in decreasing order: dot, fdp, circo, osage. "
320 "NOTE: '-f dot' and '-f dot -l dot' are different: the former "
321 "will dump a raw DOT graph and stop; the latter will further "
322 "filter it through 'dot -Tdot -Kdot' layout engine.")
323 option_parser
.add_option(
325 default
="^.*$", dest
="incl",
326 help="Include only edges of the graph that match the specified regexp. "
327 "The regexp is applied to edges of the graph formatted as "
328 "'source_node->target_node', where the '->' part is vebatim. "
329 "Therefore, a reliable regexp should look like "
330 "'^(chrome|chrome/browser|chrome/common)->content/public/browser$' "
331 "or similar, with both source and target node regexps present, "
332 "explicit ^ and $, and otherwise being as specific as possible.")
333 option_parser
.add_option(
335 default
="^$", dest
="excl",
336 help="Exclude dependent nodes that match the specified regexp. "
337 "See --incl for details on the format.")
338 option_parser
.add_option(
340 default
="", dest
="hilite_fanins",
341 help="Highlight fanins of nodes matching the specified regexp.")
342 option_parser
.add_option(
344 default
="", dest
="hilite_fanouts",
345 help="Highlight fanouts of nodes matching the specified regexp.")
346 option_parser
.add_option(
347 "", "--ignore-temp-rules",
348 action
="store_true", dest
="ignore_temp_rules", default
=False,
349 help="Ignore !-prefixed (temporary) rules in DEPS files.")
350 option_parser
.add_option(
351 "", "--ignore-specific-rules",
352 action
="store_true", dest
="ignore_specific_rules", default
=False,
353 help="Ignore specific_include_rules section of DEPS files.")
354 option_parser
.add_option(
355 "", "--hide-disallowed-deps",
356 action
="store_true", dest
="hide_disallowed_deps", default
=False,
357 help="Hide disallowed dependencies in the output graph.")
358 option_parser
.add_option(
360 action
="store_true", dest
="unflatten_graph", default
=False,
361 help="Try to reformat the output graph so it is narrower and taller. "
362 "Helps fight overly flat and wide graphs, but sometimes produces "
364 option_parser
.add_option(
366 action
="store_true", default
=False,
367 help="Print debug logging")
368 options
, args
= option_parser
.parse_args()
370 if not options
.out_file
.endswith(options
.out_format
):
371 options
.out_file
+= '.' + options
.out_format
373 deps_grapher
= DepsGrapher(
374 base_directory
=options
.base_directory
,
375 verbose
=options
.verbose
,
378 ignore_temp_rules
=options
.ignore_temp_rules
,
379 ignore_specific_rules
=options
.ignore_specific_rules
,
380 hide_disallowed_deps
=options
.hide_disallowed_deps
,
382 out_file
=options
.out_file
,
383 out_format
=options
.out_format
,
384 layout_engine
=options
.layout_engine
,
385 unflatten_graph
=options
.unflatten_graph
,
389 hilite_fanins
=options
.hilite_fanins
,
390 hilite_fanouts
=options
.hilite_fanouts
)
396 print 'Using base directory: ', deps_grapher
.base_directory
397 print 'include nodes : ', options
.incl
398 print 'exclude nodes : ', options
.excl
399 print 'highlight fanins of : ', options
.hilite_fanins
400 print 'highlight fanouts of: ', options
.hilite_fanouts
402 deps_grapher
.DumpDependencies()
406 if '__main__' == __name__
: