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 7by the DEPS files in the source tree. Supports DOT and PNG as the output format. 8 9Enables filtering and differential highlighting of parts of the graph based on 10the specified criteria. This allows for a much easier visual analysis of the 11dependencies, including answering questions such as "if a new source must 12depend on modules A, B, and C, what valid options among the existing modules 13are there to put it in." 14 15See README.md for a detailed description of the DEPS format. 16""" 17 18import os 19import optparse 20import pipes 21import re 22import sys 23 24from builddeps import DepsBuilder 25from rules import Rule 26 27 28class 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 extra_repos, 40 verbose, 41 being_tested, 42 ignore_temp_rules, 43 ignore_specific_rules, 44 hide_disallowed_deps, 45 out_file, 46 out_format, 47 layout_engine, 48 unflatten_graph, 49 incl, 50 excl, 51 hilite_fanins, 52 hilite_fanouts): 53 """Creates a new DepsGrapher. 54 55 Args: 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 70 is also included. 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. 77 """ 78 DepsBuilder.__init__( 79 self, 80 base_directory, 81 extra_repos, 82 verbose, 83 being_tested, 84 ignore_temp_rules, 85 ignore_specific_rules) 86 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 94 self.incl = incl 95 self.excl = excl 96 self.hilite_fanins = hilite_fanins 97 self.hilite_fanouts = hilite_fanouts 98 99 self.deps = set() 100 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() 106 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) 115 116 def _DumpDependencies(self): 117 """Dumps the built dependency graph to the specified file with specified 118 format.""" 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') 124 else: 125 out = open(self.out_file, 'w') 126 else: 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') 135 136 self._DumpDependenciesImpl(self.deps, out) 137 out.close() 138 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|.""" 142 deps_graph = dict() 143 deps_srcs = set() 144 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: 148 continue 149 150 deps_srcs.add(src) 151 if src not in deps_graph: 152 deps_graph[src] = [] 153 deps_graph[src].append((dst, allow)) 154 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) 159 while parent_src: 160 if parent_src not in deps_graph: 161 deps_graph[parent_src] = [] 162 parent_src = os.path.dirname(parent_src) 163 164 # For every node, propagate its rules down to all its children. 165 deps_srcs = list(deps_srcs) 166 deps_srcs.sort() 167 for src in deps_srcs: 168 parent_src = os.path.dirname(src) 169 if parent_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)) 180 181 node_props = {} 182 edges = [] 183 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, 189 # and in what way. 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: 193 continue 194 195 if allow == Rule.ALLOW and src == dst: 196 continue 197 198 edge_spec = "%s->%s" % (src, dst) 199 if not re.search(self.incl, edge_spec) or \ 200 re.search(self.excl, edge_spec): 201 continue 202 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} 207 208 edge_weight = 1 209 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 214 edge_weight += 1 215 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 220 edge_weight += 1 221 222 if allow == Rule.ALLOW: 223 edge_color = (edge_weight > 1) and 'blue' or 'green' 224 edge_style = 'solid' 225 elif allow == Rule.TEMP_ALLOW: 226 edge_color = (edge_weight > 1) and 'blue' or 'green' 227 edge_style = 'dashed' 228 else: 229 edge_color = 'red' 230 edge_style = 'dashed' 231 edges.append(' "%s" -> "%s" [style=%s,color=%s,penwidth=%d];' % \ 232 (src, dst, edge_style, edge_color, edge_weight)) 233 234 # Reformat the computed raw node attributes into a final DOT representation. 235 nodes = [] 236 for (node, attrs) in node_props.items(): 237 attr_strs = [] 238 if attrs['hilite']: 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))) 242 243 # Output nodes and edges to |out| (can be a file or a pipe). 244 edges.sort() 245 nodes.sort() 246 out.write('digraph DEPS {\n' 247 ' fontsize=8;\n') 248 out.write('\n'.join(nodes)) 249 out.write('\n\n') 250 out.write('\n'.join(edges)) 251 out.write('\n}\n') 252 out.close() 253 254 255def PrintUsage(): 256 print("""Usage: python graphdeps.py [--root <root>] 257 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". 261 262 --(others) There are a few lesser-used options; run with --help to show them. 263 264Examples: 265 Dump the whole dependency graph: 266 graphdeps.py 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: 275 graphdeps.py \ 276 --root=./src \ 277 --out=./DEPS.svg \ 278 --format=svg \ 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""") 284 285 286def main(): 287 option_parser = optparse.OptionParser() 288 option_parser.add_option( 289 "", "--root", 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( 295 '', '--extra-repos', 296 action='append', dest='extra_repos', default=[], 297 help='Specifies extra repositories relative to root repository.') 298 option_parser.add_option( 299 "-f", "--format", 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( 307 "-o", "--out", 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 " 311 "appended.") 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( 322 "-i", "--incl", 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( 332 "-e", "--excl", 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( 337 "", "--fanin", 338 default="", dest="hilite_fanins", 339 help="Highlight fanins of nodes matching the specified regexp.") 340 option_parser.add_option( 341 "", "--fanout", 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( 357 "", "--unflatten", 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 " 361 "inferior results.") 362 option_parser.add_option( 363 "-v", "--verbose", 364 action="store_true", default=False, 365 help="Print debug logging") 366 options, args = option_parser.parse_args() 367 368 if not options.out_file.endswith(options.out_format): 369 options.out_file += '.' + options.out_format 370 371 deps_grapher = DepsGrapher( 372 base_directory=options.base_directory, 373 extra_repos=options.extra_repos, 374 verbose=options.verbose, 375 being_tested=False, 376 377 ignore_temp_rules=options.ignore_temp_rules, 378 ignore_specific_rules=options.ignore_specific_rules, 379 hide_disallowed_deps=options.hide_disallowed_deps, 380 381 out_file=options.out_file, 382 out_format=options.out_format, 383 layout_engine=options.layout_engine, 384 unflatten_graph=options.unflatten_graph, 385 386 incl=options.incl, 387 excl=options.excl, 388 hilite_fanins=options.hilite_fanins, 389 hilite_fanouts=options.hilite_fanouts) 390 391 if len(args) > 0: 392 PrintUsage() 393 return 1 394 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) 400 401 deps_grapher.DumpDependencies() 402 return 0 403 404 405if '__main__' == __name__: 406 sys.exit(main()) 407