OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 The Chromium OS 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 """Generates pretty dependency graphs for Chrome OS packages.""" |
| 7 |
| 8 import optparse |
| 9 import os |
| 10 import subprocess |
| 11 import sys |
| 12 |
| 13 |
| 14 NORMAL_COLOR = 'black' |
| 15 TARGET_COLOR = 'red' |
| 16 SEED_COLOR = 'green' |
| 17 CHILD_COLOR = 'grey' |
| 18 |
| 19 |
| 20 def GetReverseDependencyClosure(full_name, deps_map): |
| 21 """Gets the closure of the reverse dependencies of a node. |
| 22 |
| 23 Walks the tree along all the reverse dependency paths to find all the nodes |
| 24 that transitively depend on the input node.""" |
| 25 s = set() |
| 26 def GetClosure(name): |
| 27 s.add(name) |
| 28 node = deps_map[name] |
| 29 for dep in node['rev_deps']: |
| 30 if dep in s: |
| 31 continue |
| 32 GetClosure(dep) |
| 33 |
| 34 GetClosure(full_name) |
| 35 return s |
| 36 |
| 37 |
| 38 def GetOutputBaseName(node, options): |
| 39 """Gets the basename of the output file for a node.""" |
| 40 return '%s_%s-%s.%s' % (node['category'], node['name'], node['version'], |
| 41 options.format) |
| 42 |
| 43 |
| 44 def GetNodeLines(node, options, color): |
| 45 """Gets the dot definition for a node.""" |
| 46 name = node['full_name'] |
| 47 tags = ['label="%s (%s)"' % (name, node['action']), |
| 48 'color="%s"' % color, |
| 49 'fontcolor="%s"' % color] |
| 50 if options.link: |
| 51 filename = GetOutputBaseName(node, options) |
| 52 tags.append('href="%s%s"' % (options.base_url, filename)) |
| 53 return ['"%s" [%s];' % (name, ', '.join(tags))] |
| 54 |
| 55 |
| 56 def GetReverseDependencyArcLines(node, options): |
| 57 """Gets the dot definitions for the arcs leading to a node.""" |
| 58 lines = [] |
| 59 name = node['full_name'] |
| 60 for j in node['rev_deps']: |
| 61 lines.append('"%s" -> "%s";' % (j, name)) |
| 62 return lines |
| 63 |
| 64 |
| 65 def GenerateDotGraph(package, deps_map, options): |
| 66 """Generates the dot source for the dependency graph leading to a node. |
| 67 |
| 68 The output is a list of lines.""" |
| 69 deps = GetReverseDependencyClosure(package, deps_map) |
| 70 node = deps_map[package] |
| 71 |
| 72 # Keep track of all the emitted nodes so that we don't issue multiple |
| 73 # definitions |
| 74 emitted = set() |
| 75 |
| 76 lines = ['digraph dep {', |
| 77 'graph [name="%s"];' % package] |
| 78 |
| 79 # Add all the children if we want them, all of them in their own subgraph, |
| 80 # as a sink. Keep the arcs outside of the subgraph though (it generates |
| 81 # better layout). |
| 82 has_children = False |
| 83 if options.children and node['deps']: |
| 84 has_children = True |
| 85 lines += ['subgraph {', |
| 86 'rank=sink;'] |
| 87 arc_lines = [] |
| 88 for child in node['deps']: |
| 89 child_node = deps_map[child] |
| 90 lines += GetNodeLines(child_node, options, CHILD_COLOR) |
| 91 emitted.add(child) |
| 92 # If child is in the rev_deps, we'll get the arc later. |
| 93 if not child in node['rev_deps']: |
| 94 arc_lines.append('"%s" -> "%s";' % (package, child)) |
| 95 lines += ['}'] |
| 96 lines += arc_lines |
| 97 |
| 98 # Add the package in its own subgraph. If we didn't have children, make it |
| 99 # a sink |
| 100 lines += ['subgraph {'] |
| 101 if has_children: |
| 102 lines += ['rank=same;'] |
| 103 else: |
| 104 lines += ['rank=sink;'] |
| 105 lines += GetNodeLines(node, options, TARGET_COLOR) |
| 106 emitted.add(package) |
| 107 lines += ['}'] |
| 108 |
| 109 # Add all the other nodes, as well as all the arcs. |
| 110 for dep in deps: |
| 111 dep_node = deps_map[dep] |
| 112 if not dep in emitted: |
| 113 color = NORMAL_COLOR |
| 114 if dep_node['action'] == 'seed': |
| 115 color = SEED_COLOR |
| 116 lines += GetNodeLines(dep_node, options, color) |
| 117 lines += GetReverseDependencyArcLines(dep_node, options) |
| 118 |
| 119 lines += ['}'] |
| 120 return lines |
| 121 |
| 122 |
| 123 def GenerateImages(input, options): |
| 124 """Generate the output images for all the nodes in the input.""" |
| 125 deps_map = eval(input.read()) |
| 126 |
| 127 for package in deps_map: |
| 128 lines = GenerateDotGraph(package, deps_map, options) |
| 129 data = '\n'.join(lines) |
| 130 |
| 131 filename = os.path.join(options.output_dir, |
| 132 GetOutputBaseName(deps_map[package], options)) |
| 133 |
| 134 # Send the source to dot. |
| 135 proc = subprocess.Popen(['dot', '-T' + options.format, '-o' + filename], |
| 136 stdin=subprocess.PIPE) |
| 137 proc.communicate(data) |
| 138 |
| 139 if options.save_dot: |
| 140 file = open(filename + '.dot', 'w') |
| 141 file.write(data) |
| 142 file.close() |
| 143 |
| 144 |
| 145 def main(): |
| 146 parser = optparse.OptionParser(usage='usage: %prog [options] input') |
| 147 parser.add_option('-f', '--format', default='svg', |
| 148 help='Dot output format (png, svg, etc.).') |
| 149 parser.add_option('-o', '--output-dir', default='.', |
| 150 help='Output directory.') |
| 151 parser.add_option('-c', '--children', action='store_true', |
| 152 help='Also add children.') |
| 153 parser.add_option('-l', '--link', action='store_true', |
| 154 help='Embed links.') |
| 155 parser.add_option('-b', '--base-url', default='', |
| 156 help='Base url for links.') |
| 157 parser.add_option('-s', '--save-dot', action='store_true', |
| 158 help='Save dot files.') |
| 159 (options, inputs) = parser.parse_args() |
| 160 |
| 161 try: |
| 162 os.makedirs(options.output_dir) |
| 163 except OSError: |
| 164 # The directory already exists. |
| 165 pass |
| 166 |
| 167 if not inputs: |
| 168 GenerateImages(sys.stdin, options) |
| 169 else: |
| 170 for i in inputs: |
| 171 file = open(i) |
| 172 GenerateImages(file, options) |
| 173 file.close() |
| 174 |
| 175 if __name__ == '__main__': |
| 176 main() |
OLD | NEW |