diff --git a/cros_deps_diff b/cros_deps_diff new file mode 100755 index 0000000000..b18a92ab8f --- /dev/null +++ b/cros_deps_diff @@ -0,0 +1,182 @@ +#!/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 dependency graph diffs. + +As an input it takes 2 or more dependency graphs output from cros_extract_deps +and it finds all divergent packages (packages whose versions differ between +some of these dependency graphs) and outputs graphs that trace the divergence +in the dependency trees until common packages are found. +""" + +import dot_helper +import optparse +import os + +NORMAL_COLOR = 'black' +BASE_COLORS = ['red', 'green', 'blue'] + + +def UnversionedName(dep): + """Returns the name of the package, omitting the version.""" + return '%s/%s' % (dep['category'], dep['name']) + + +def GetColor(index): + """Maps index to a color.""" + try: + return BASE_COLORS[index] + except IndexError: + # Generate a color by splicing the bits to generate high contrast colors + index -= len(BASE_COLORS) - 1 + chars = [0] * 3 + for bit in xrange(0, 24): + chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3) + return "#%02x%02x%02x" % tuple(chars) + + +def GetReverseDependencyClosure(full_name, deps_map, divergent_set): + """Gets the closure of the reverse dependencies of a node. + + Walks the tree along all the reverse dependency paths to find all the nodes + of the divergent set that transitively depend on the input node.""" + s = set() + def GetClosure(name): + node = deps_map[name] + if UnversionedName(node) in divergent_set: + s.add(name) + for dep in node['rev_deps']: + if dep in s: + continue + GetClosure(dep) + + GetClosure(full_name) + return s + + +def GetVersionMap(input_deps): + """Creates the version map for the input data. + + The version map maps an unversioned package name to its corresponding + versioned name depending on the input dependency graph. + + For every package, it maps the input data index to the full name (versioned) + of the package in that input data. E.g. + map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203', + 1:'x11-base/xorg-server-1.7.6-r8'} + """ + version_map = {} + i = 0 + for deps_map in input_deps: + for full_name, dep in deps_map.iteritems(): + pkg = UnversionedName(dep) + entry = version_map.setdefault(pkg, {}) + entry[i] = full_name + i += 1 + return version_map + + +def GetDivergentSet(version_map, count): + """Gets the set of divergent packages. + + Divergent packages are those that have a different version among the input + dependency graphs (or missing version altogether).""" + divergent_set = set() + for pkg, value in version_map.iteritems(): + if len(value.keys()) != count or len(set(value.values())) > 1: + # The package doesn't exist for at least one ot the input, or there are + # more than 2 versions. + divergent_set.add(pkg) + return divergent_set + + +def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set): + graph = dot_helper.Graph(pkg) + + # A subgraph for the divergent package we're considering. Add all its + # versions as a sink. + pkg_subgraph = graph.AddNewSubgraph('sink') + + # The outer packages are those that aren't divergent but depend on a + # divergent package. Add them in their own subgraph, as sources. + outer_subgraph = graph.AddNewSubgraph('source') + + emitted = set() + for i in xrange(0, len(input_deps)): + try: + pkg_name = version_map[pkg][i] + except KeyError: + continue + + color = GetColor(i) + + if pkg_name not in emitted: + pkg_subgraph.AddNode(pkg_name, pkg_name, color, None) + emitted.add(pkg_name) + + # Add one subgraph per version for generally better layout. + subgraph = graph.AddNewSubgraph() + + nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set) + for node_name in nodes: + if node_name not in emitted: + subgraph.AddNode(node_name, node_name, color, None) + emitted.add(node_name) + + # Add outer packages, and all the arcs. + for dep in input_deps[i][node_name]['rev_deps']: + dep_node = input_deps[i][dep] + if UnversionedName(dep_node) not in divergent_set and dep not in emitted: + outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None) + emitted.add(dep) + graph.AddArc(dep, node_name) + + return graph + + +def main(): + parser = optparse.OptionParser( + usage='usage: %prog [options] input1 input2...') + 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('-s', '--save-dot', action='store_true', + help='Save dot files.') + options, inputs = parser.parse_args() + + input_deps = [] + for i in inputs: + file = open(i) + input_deps.append(eval(file.read())) + file.close() + + version_map = GetVersionMap(input_deps) + divergent_set = GetDivergentSet(version_map, len(input_deps)) + + # Get all the output directories + all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set) + + for i in all_dirs: + try: + os.makedirs(os.path.join(options.output_dir, i)) + except OSError: + # The directory already exists. + pass + + for pkg in divergent_set: + filename = os.path.join(options.output_dir, pkg) + '.' + options.format + + save_dot_filename = None + if options.save_dot: + save_dot_filename = filename + '.dot' + + graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set) + lines = graph.Gen() + dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename) + + +if __name__ == '__main__': + main()