| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 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. | |
| 5 | |
| 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. | |
| 8 | |
| 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." | |
| 14 | |
| 15 See builddeps.py for a detailed description of the DEPS format. | |
| 16 """ | |
| 17 | |
| 18 import os | |
| 19 import optparse | |
| 20 import pipes | |
| 21 import re | |
| 22 import sys | |
| 23 | |
| 24 from builddeps import DepsBuilder | |
| 25 from rules import Rule | |
| 26 | |
| 27 | |
| 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. | |
| 35 """ | |
| 36 | |
| 37 def __init__(self, | |
| 38 base_directory, | |
| 39 verbose, | |
| 40 being_tested, | |
| 41 ignore_temp_rules, | |
| 42 ignore_specific_rules, | |
| 43 hide_disallowed_deps, | |
| 44 out_file, | |
| 45 out_format, | |
| 46 layout_engine, | |
| 47 unflatten_graph, | |
| 48 incl, | |
| 49 excl, | |
| 50 hilite_fanins, | |
| 51 hilite_fanouts): | |
| 52 """Creates a new DepsGrapher. | |
| 53 | |
| 54 Args: | |
| 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 | |
| 69 is also included. | |
| 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. | |
| 76 """ | |
| 77 DepsBuilder.__init__( | |
| 78 self, | |
| 79 base_directory, | |
| 80 verbose, | |
| 81 being_tested, | |
| 82 ignore_temp_rules, | |
| 83 ignore_specific_rules) | |
| 84 | |
| 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 | |
| 92 self.incl = incl | |
| 93 self.excl = excl | |
| 94 self.hilite_fanins = hilite_fanins | |
| 95 self.hilite_fanouts = hilite_fanouts | |
| 96 | |
| 97 self.deps = set() | |
| 98 | |
| 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() | |
| 104 | |
| 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) | |
| 111 if rules: | |
| 112 deps = rules.AsDependencyTuples( | |
| 113 include_general_rules=True, | |
| 114 include_specific_rules=not self.ignore_specific_rules) | |
| 115 self.deps.update(deps) | |
| 116 | |
| 117 for item in sorted(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) | |
| 121 | |
| 122 def _DumpDependencies(self): | |
| 123 """Dumps the built dependency graph to the specified file with specified | |
| 124 format.""" | |
| 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') | |
| 130 else: | |
| 131 out = open(self.out_file, 'w') | |
| 132 else: | |
| 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') | |
| 141 | |
| 142 self._DumpDependenciesImpl(self.deps, out) | |
| 143 out.close() | |
| 144 | |
| 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|.""" | |
| 148 deps_graph = dict() | |
| 149 deps_srcs = set() | |
| 150 | |
| 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: | |
| 154 continue | |
| 155 | |
| 156 deps_srcs.add(src) | |
| 157 if src not in deps_graph: | |
| 158 deps_graph[src] = [] | |
| 159 deps_graph[src].append((dst, allow)) | |
| 160 | |
| 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) | |
| 165 while parent_src: | |
| 166 if parent_src not in deps_graph: | |
| 167 deps_graph[parent_src] = [] | |
| 168 parent_src = os.path.dirname(parent_src) | |
| 169 | |
| 170 # For every node, propagate its rules down to all its children. | |
| 171 deps_srcs = list(deps_srcs) | |
| 172 deps_srcs.sort() | |
| 173 for src in deps_srcs: | |
| 174 parent_src = os.path.dirname(src) | |
| 175 if parent_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)) | |
| 186 | |
| 187 node_props = {} | |
| 188 edges = [] | |
| 189 | |
| 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, | |
| 195 # and in what way. | |
| 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: | |
| 199 continue | |
| 200 | |
| 201 if allow == Rule.ALLOW and src == dst: | |
| 202 continue | |
| 203 | |
| 204 edge_spec = "%s->%s" % (src, dst) | |
| 205 if not re.search(self.incl, edge_spec) or \ | |
| 206 re.search(self.excl, edge_spec): | |
| 207 continue | |
| 208 | |
| 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} | |
| 213 | |
| 214 edge_weight = 1 | |
| 215 | |
| 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 | |
| 220 edge_weight += 1 | |
| 221 | |
| 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 | |
| 226 edge_weight += 1 | |
| 227 | |
| 228 if allow == Rule.ALLOW: | |
| 229 edge_color = (edge_weight > 1) and 'blue' or 'green' | |
| 230 edge_style = 'solid' | |
| 231 elif allow == Rule.TEMP_ALLOW: | |
| 232 edge_color = (edge_weight > 1) and 'blue' or 'green' | |
| 233 edge_style = 'dashed' | |
| 234 else: | |
| 235 edge_color = 'red' | |
| 236 edge_style = 'dashed' | |
| 237 edges.append(' "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \ | |
| 238 (src, dst, edge_style, edge_color, edge_weight)) | |
| 239 | |
| 240 # Reformat the computed raw node attributes into a final DOT representation. | |
| 241 nodes = [] | |
| 242 for (node, attrs) in node_props.iteritems(): | |
| 243 attr_strs = [] | |
| 244 if attrs['hilite']: | |
| 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))) | |
| 248 | |
| 249 # Output nodes and edges to |out| (can be a file or a pipe). | |
| 250 edges.sort() | |
| 251 nodes.sort() | |
| 252 out.write('digraph DEPS {\n' | |
| 253 ' fontsize=8;\n') | |
| 254 out.write('\n'.join(nodes)) | |
| 255 out.write('\n\n') | |
| 256 out.write('\n'.join(edges)) | |
| 257 out.write('\n}\n') | |
| 258 out.close() | |
| 259 | |
| 260 | |
| 261 def PrintUsage(): | |
| 262 print """Usage: python graphdeps.py [--root <root>] | |
| 263 | |
| 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". | |
| 267 | |
| 268 --(others) There are a few lesser-used options; run with --help to show them. | |
| 269 | |
| 270 Examples: | |
| 271 Dump the whole dependency graph: | |
| 272 graphdeps.py | |
| 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: | |
| 281 graphdeps.py \ | |
| 282 --root=./src \ | |
| 283 --out=./DEPS.svg \ | |
| 284 --format=svg \ | |
| 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""" | |
| 290 | |
| 291 | |
| 292 def main(): | |
| 293 option_parser = optparse.OptionParser() | |
| 294 option_parser.add_option( | |
| 295 "", "--root", | |
| 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( | |
| 301 "-f", "--format", | |
| 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( | |
| 309 "-o", "--out", | |
| 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 " | |
| 313 "appended.") | |
| 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( | |
| 324 "-i", "--incl", | |
| 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( | |
| 334 "-e", "--excl", | |
| 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( | |
| 339 "", "--fanin", | |
| 340 default="", dest="hilite_fanins", | |
| 341 help="Highlight fanins of nodes matching the specified regexp.") | |
| 342 option_parser.add_option( | |
| 343 "", "--fanout", | |
| 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( | |
| 359 "", "--unflatten", | |
| 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 " | |
| 363 "inferior results.") | |
| 364 option_parser.add_option( | |
| 365 "-v", "--verbose", | |
| 366 action="store_true", default=False, | |
| 367 help="Print debug logging") | |
| 368 options, args = option_parser.parse_args() | |
| 369 | |
| 370 if not options.out_file.endswith(options.out_format): | |
| 371 options.out_file += '.' + options.out_format | |
| 372 | |
| 373 deps_grapher = DepsGrapher( | |
| 374 base_directory=options.base_directory, | |
| 375 verbose=options.verbose, | |
| 376 being_tested=False, | |
| 377 | |
| 378 ignore_temp_rules=options.ignore_temp_rules, | |
| 379 ignore_specific_rules=options.ignore_specific_rules, | |
| 380 hide_disallowed_deps=options.hide_disallowed_deps, | |
| 381 | |
| 382 out_file=options.out_file, | |
| 383 out_format=options.out_format, | |
| 384 layout_engine=options.layout_engine, | |
| 385 unflatten_graph=options.unflatten_graph, | |
| 386 | |
| 387 incl=options.incl, | |
| 388 excl=options.excl, | |
| 389 hilite_fanins=options.hilite_fanins, | |
| 390 hilite_fanouts=options.hilite_fanouts) | |
| 391 | |
| 392 if len(args) > 0: | |
| 393 PrintUsage() | |
| 394 return 1 | |
| 395 | |
| 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 | |
| 401 | |
| 402 deps_grapher.DumpDependencies() | |
| 403 return 0 | |
| 404 | |
| 405 | |
| 406 if '__main__' == __name__: | |
| 407 sys.exit(main()) | |
| OLD | NEW |