Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame^] | 1 | # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Generates dependency graph diffs. |
| 6 | |
| 7 | As an input it takes 2 or more dependency graphs output from cros_extract_deps |
| 8 | and it finds all divergent packages (packages whose versions differ between |
| 9 | some of these dependency graphs) and outputs graphs that trace the divergence |
| 10 | in the dependency trees until common packages are found. |
| 11 | """ |
| 12 | |
| 13 | import json |
| 14 | import optparse |
| 15 | import os |
| 16 | |
| 17 | from chromite.lib import dot_helper |
| 18 | |
| 19 | NORMAL_COLOR = 'black' |
| 20 | BASE_COLORS = ['red', 'green', 'blue'] |
| 21 | |
| 22 | |
| 23 | def UnversionedName(dep): |
| 24 | """Returns the name of the package, omitting the version.""" |
| 25 | return '%s/%s' % (dep['category'], dep['name']) |
| 26 | |
| 27 | |
| 28 | def GetColor(index): |
| 29 | """Maps index to a color.""" |
| 30 | try: |
| 31 | return BASE_COLORS[index] |
| 32 | except IndexError: |
| 33 | # Generate a color by splicing the bits to generate high contrast colors |
| 34 | index -= len(BASE_COLORS) - 1 |
| 35 | chars = [0] * 3 |
| 36 | for bit in xrange(0, 24): |
| 37 | chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3) |
| 38 | return "#%02x%02x%02x" % tuple(chars) |
| 39 | |
| 40 | |
| 41 | def GetReverseDependencyClosure(full_name, deps_map, divergent_set): |
| 42 | """Gets the closure of the reverse dependencies of a node. |
| 43 | |
| 44 | Walks the tree along all the reverse dependency paths to find all the nodes |
| 45 | of the divergent set that transitively depend on the input node.""" |
| 46 | s = set() |
| 47 | def GetClosure(name): |
| 48 | node = deps_map[name] |
| 49 | if UnversionedName(node) in divergent_set: |
| 50 | s.add(name) |
| 51 | for dep in node['rev_deps']: |
| 52 | if dep in s: |
| 53 | continue |
| 54 | GetClosure(dep) |
| 55 | |
| 56 | GetClosure(full_name) |
| 57 | return s |
| 58 | |
| 59 | |
| 60 | def GetVersionMap(input_deps): |
| 61 | """Creates the version map for the input data. |
| 62 | |
| 63 | The version map maps an unversioned package name to its corresponding |
| 64 | versioned name depending on the input dependency graph. |
| 65 | |
| 66 | For every package, it maps the input data index to the full name (versioned) |
| 67 | of the package in that input data. E.g. |
| 68 | map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203', |
| 69 | 1:'x11-base/xorg-server-1.7.6-r8'} |
| 70 | """ |
| 71 | version_map = {} |
| 72 | i = 0 |
| 73 | for deps_map in input_deps: |
| 74 | for full_name, dep in deps_map.iteritems(): |
| 75 | pkg = UnversionedName(dep) |
| 76 | entry = version_map.setdefault(pkg, {}) |
| 77 | entry[i] = full_name |
| 78 | i += 1 |
| 79 | return version_map |
| 80 | |
| 81 | |
| 82 | def GetDivergentSet(version_map, count): |
| 83 | """Gets the set of divergent packages. |
| 84 | |
| 85 | Divergent packages are those that have a different version among the input |
| 86 | dependency graphs (or missing version altogether).""" |
| 87 | divergent_set = set() |
| 88 | for pkg, value in version_map.iteritems(): |
| 89 | if len(value.keys()) != count or len(set(value.values())) > 1: |
| 90 | # The package doesn't exist for at least one ot the input, or there are |
| 91 | # more than 2 versions. |
| 92 | divergent_set.add(pkg) |
| 93 | return divergent_set |
| 94 | |
| 95 | |
| 96 | def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set): |
| 97 | graph = dot_helper.Graph(pkg) |
| 98 | |
| 99 | # A subgraph for the divergent package we're considering. Add all its |
| 100 | # versions as a sink. |
| 101 | pkg_subgraph = graph.AddNewSubgraph('sink') |
| 102 | |
| 103 | # The outer packages are those that aren't divergent but depend on a |
| 104 | # divergent package. Add them in their own subgraph, as sources. |
| 105 | outer_subgraph = graph.AddNewSubgraph('source') |
| 106 | |
| 107 | emitted = set() |
| 108 | for i in xrange(0, len(input_deps)): |
| 109 | try: |
| 110 | pkg_name = version_map[pkg][i] |
| 111 | except KeyError: |
| 112 | continue |
| 113 | |
| 114 | color = GetColor(i) |
| 115 | |
| 116 | if pkg_name not in emitted: |
| 117 | pkg_subgraph.AddNode(pkg_name, pkg_name, color, None) |
| 118 | emitted.add(pkg_name) |
| 119 | |
| 120 | # Add one subgraph per version for generally better layout. |
| 121 | subgraph = graph.AddNewSubgraph() |
| 122 | |
| 123 | nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set) |
| 124 | for node_name in nodes: |
| 125 | if node_name not in emitted: |
| 126 | subgraph.AddNode(node_name, node_name, color, None) |
| 127 | emitted.add(node_name) |
| 128 | |
| 129 | # Add outer packages, and all the arcs. |
| 130 | for dep in input_deps[i][node_name]['rev_deps']: |
| 131 | dep_node = input_deps[i][dep] |
| 132 | if (UnversionedName(dep_node) not in divergent_set |
| 133 | and dep not in emitted): |
| 134 | outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None) |
| 135 | emitted.add(dep) |
| 136 | graph.AddArc(dep, node_name) |
| 137 | |
| 138 | return graph |
| 139 | |
| 140 | |
| 141 | def main(argv): |
| 142 | parser = optparse.OptionParser( |
| 143 | usage='usage: %prog [options] input1 input2...') |
| 144 | parser.add_option('-f', '--format', default='svg', |
| 145 | help='Dot output format (png, svg, etc.).') |
| 146 | parser.add_option('-o', '--output-dir', default='.', |
| 147 | help='Output directory.') |
| 148 | parser.add_option('-s', '--save-dot', action='store_true', |
| 149 | help='Save dot files.') |
| 150 | options, inputs = parser.parse_args(argv) |
| 151 | |
| 152 | input_deps = [] |
| 153 | for i in inputs: |
| 154 | with open(i) as handle: |
| 155 | input_deps.append(json.loads(handle.read())) |
| 156 | |
| 157 | version_map = GetVersionMap(input_deps) |
| 158 | divergent_set = GetDivergentSet(version_map, len(input_deps)) |
| 159 | |
| 160 | # Get all the output directories |
| 161 | all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set) |
| 162 | |
| 163 | for i in all_dirs: |
| 164 | try: |
| 165 | os.makedirs(os.path.join(options.output_dir, i)) |
| 166 | except OSError: |
| 167 | # The directory already exists. |
| 168 | pass |
| 169 | |
| 170 | for pkg in divergent_set: |
| 171 | filename = os.path.join(options.output_dir, pkg) + '.' + options.format |
| 172 | |
| 173 | save_dot_filename = None |
| 174 | if options.save_dot: |
| 175 | save_dot_filename = filename + '.dot' |
| 176 | |
| 177 | graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set) |
| 178 | lines = graph.Gen() |
| 179 | dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename) |