diff --git a/cros_generate_deps_graphs b/cros_generate_deps_graphs new file mode 100755 index 0000000000..946cc38404 --- /dev/null +++ b/cros_generate_deps_graphs @@ -0,0 +1,176 @@ +#!/usr/bin/python +# Copyright (c) 2010 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Generates pretty dependency graphs for Chrome OS packages.""" + +import optparse +import os +import subprocess +import sys + + +NORMAL_COLOR = 'black' +TARGET_COLOR = 'red' +SEED_COLOR = 'green' +CHILD_COLOR = 'grey' + + +def GetReverseDependencyClosure(full_name, deps_map): + """Gets the closure of the reverse dependencies of a node. + + Walks the tree along all the reverse dependency paths to find all the nodes + that transitively depend on the input node.""" + s = set() + def GetClosure(name): + s.add(name) + node = deps_map[name] + for dep in node['rev_deps']: + if dep in s: + continue + GetClosure(dep) + + GetClosure(full_name) + return s + + +def GetOutputBaseName(node, options): + """Gets the basename of the output file for a node.""" + return '%s_%s-%s.%s' % (node['category'], node['name'], node['version'], + options.format) + + +def GetNodeLines(node, options, color): + """Gets the dot definition for a node.""" + name = node['full_name'] + tags = ['label="%s (%s)"' % (name, node['action']), + 'color="%s"' % color, + 'fontcolor="%s"' % color] + if options.link: + filename = GetOutputBaseName(node, options) + tags.append('href="%s%s"' % (options.base_url, filename)) + return ['"%s" [%s];' % (name, ', '.join(tags))] + + +def GetReverseDependencyArcLines(node, options): + """Gets the dot definitions for the arcs leading to a node.""" + lines = [] + name = node['full_name'] + for j in node['rev_deps']: + lines.append('"%s" -> "%s";' % (j, name)) + return lines + + +def GenerateDotGraph(package, deps_map, options): + """Generates the dot source for the dependency graph leading to a node. + + The output is a list of lines.""" + deps = GetReverseDependencyClosure(package, deps_map) + node = deps_map[package] + + # Keep track of all the emitted nodes so that we don't issue multiple + # definitions + emitted = set() + + lines = ['digraph dep {', + 'graph [name="%s"];' % package] + + # Add all the children if we want them, all of them in their own subgraph, + # as a sink. Keep the arcs outside of the subgraph though (it generates + # better layout). + has_children = False + if options.children and node['deps']: + has_children = True + lines += ['subgraph {', + 'rank=sink;'] + arc_lines = [] + for child in node['deps']: + child_node = deps_map[child] + lines += GetNodeLines(child_node, options, CHILD_COLOR) + emitted.add(child) + # If child is in the rev_deps, we'll get the arc later. + if not child in node['rev_deps']: + arc_lines.append('"%s" -> "%s";' % (package, child)) + lines += ['}'] + lines += arc_lines + + # Add the package in its own subgraph. If we didn't have children, make it + # a sink + lines += ['subgraph {'] + if has_children: + lines += ['rank=same;'] + else: + lines += ['rank=sink;'] + lines += GetNodeLines(node, options, TARGET_COLOR) + emitted.add(package) + lines += ['}'] + + # Add all the other nodes, as well as all the arcs. + for dep in deps: + dep_node = deps_map[dep] + if not dep in emitted: + color = NORMAL_COLOR + if dep_node['action'] == 'seed': + color = SEED_COLOR + lines += GetNodeLines(dep_node, options, color) + lines += GetReverseDependencyArcLines(dep_node, options) + + lines += ['}'] + return lines + + +def GenerateImages(input, options): + """Generate the output images for all the nodes in the input.""" + deps_map = eval(input.read()) + + for package in deps_map: + lines = GenerateDotGraph(package, deps_map, options) + data = '\n'.join(lines) + + filename = os.path.join(options.output_dir, + GetOutputBaseName(deps_map[package], options)) + + # Send the source to dot. + proc = subprocess.Popen(['dot', '-T' + options.format, '-o' + filename], + stdin=subprocess.PIPE) + proc.communicate(data) + + if options.save_dot: + file = open(filename + '.dot', 'w') + file.write(data) + file.close() + + +def main(): + parser = optparse.OptionParser(usage='usage: %prog [options] input') + parser.add_option('-f', '--format', default='svg', + help='Dot output format (png, svg, etc.).') + parser.add_option('-o', '--output-dir', default='.', + help='Output directory.') + parser.add_option('-c', '--children', action='store_true', + help='Also add children.') + parser.add_option('-l', '--link', action='store_true', + help='Embed links.') + parser.add_option('-b', '--base-url', default='', + help='Base url for links.') + parser.add_option('-s', '--save-dot', action='store_true', + help='Save dot files.') + (options, inputs) = parser.parse_args() + + try: + os.makedirs(options.output_dir) + except OSError: + # The directory already exists. + pass + + if not inputs: + GenerateImages(sys.stdin, options) + else: + for i in inputs: + file = open(i) + GenerateImages(file, options) + file.close() + +if __name__ == '__main__': + main()