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