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 |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 14 | import os |
| 15 | |
Mike Frysinger | a9436d1 | 2015-06-04 01:49:41 -0400 | [diff] [blame] | 16 | from chromite.lib import commandline |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 17 | from chromite.lib import dot_helper |
| 18 | |
Mike Frysinger | 807d828 | 2022-04-28 22:45:17 -0400 | [diff] [blame] | 19 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 20 | NORMAL_COLOR = "black" |
| 21 | BASE_COLORS = ["red", "green", "blue"] |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 22 | |
| 23 | |
| 24 | def UnversionedName(dep): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 25 | """Returns the name of the package, omitting the version.""" |
| 26 | return "%s/%s" % (dep["category"], dep["name"]) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 27 | |
| 28 | |
| 29 | def GetColor(index): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 30 | """Maps index to a color.""" |
| 31 | try: |
| 32 | return BASE_COLORS[index] |
| 33 | except IndexError: |
| 34 | # Generate a color by splicing the bits to generate high contrast colors |
| 35 | index -= len(BASE_COLORS) - 1 |
| 36 | chars = [0] * 3 |
| 37 | for bit in range(0, 24): |
| 38 | chars[bit % 3] |= ((index >> bit) & 0x1) << (7 - bit // 3) |
| 39 | return "#%02x%02x%02x" % tuple(chars) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 40 | |
| 41 | |
| 42 | def GetReverseDependencyClosure(full_name, deps_map, divergent_set): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 43 | """Gets the closure of the reverse dependencies of a node. |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 44 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 45 | Walks the tree along all the reverse dependency paths to find all the nodes |
| 46 | of the divergent set that transitively depend on the input node. |
| 47 | """ |
| 48 | s = set() |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 49 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 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 |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 61 | |
| 62 | |
| 63 | def GetVersionMap(input_deps): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 64 | """Creates the version map for the input data. |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 65 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 66 | The version map maps an unversioned package name to its corresponding |
| 67 | versioned name depending on the input dependency graph. |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 68 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 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.items(): |
| 78 | pkg = UnversionedName(dep) |
| 79 | entry = version_map.setdefault(pkg, {}) |
| 80 | entry[i] = full_name |
| 81 | i += 1 |
| 82 | return version_map |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 83 | |
| 84 | |
| 85 | def GetDivergentSet(version_map, count): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 86 | """Gets the set of divergent packages. |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 87 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 88 | Divergent packages are those that have a different version among the input |
| 89 | dependency graphs (or missing version altogether). |
| 90 | """ |
| 91 | divergent_set = set() |
| 92 | for pkg, value in version_map.items(): |
| 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 |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 98 | |
| 99 | |
| 100 | def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 101 | graph = dot_helper.Graph(pkg) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 102 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 103 | # A subgraph for the divergent package we're considering. Add all its |
| 104 | # versions as a sink. |
| 105 | pkg_subgraph = graph.AddNewSubgraph("sink") |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 106 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 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") |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 110 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 111 | emitted = set() |
| 112 | for i, input_dep in enumerate(input_deps): |
| 113 | try: |
| 114 | pkg_name = version_map[pkg][i] |
| 115 | except KeyError: |
| 116 | continue |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 117 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 118 | color = GetColor(i) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 119 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 120 | if pkg_name not in emitted: |
| 121 | pkg_subgraph.AddNode(pkg_name, pkg_name, color, None) |
| 122 | emitted.add(pkg_name) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 123 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 124 | # Add one subgraph per version for generally better layout. |
| 125 | subgraph = graph.AddNewSubgraph() |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 126 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 127 | nodes = GetReverseDependencyClosure(pkg_name, input_dep, 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) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 132 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 133 | # Add outer packages, and all the arcs. |
| 134 | for dep in input_dep[node_name]["rev_deps"]: |
| 135 | dep_node = input_dep[dep] |
| 136 | if ( |
| 137 | UnversionedName(dep_node) not in divergent_set |
| 138 | and dep not in emitted |
| 139 | ): |
| 140 | outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None) |
| 141 | emitted.add(dep) |
| 142 | graph.AddArc(dep, node_name) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 143 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 144 | return graph |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 145 | |
| 146 | |
| 147 | def main(argv): |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 148 | parser = commandline.ArgumentParser(description=__doc__) |
| 149 | parser.add_argument( |
| 150 | "-f", |
| 151 | "--format", |
| 152 | default="svg", |
| 153 | help="Dot output format (png, svg, etc.).", |
| 154 | ) |
| 155 | parser.add_argument( |
| 156 | "-o", "--output-dir", default=".", help="Output directory." |
| 157 | ) |
| 158 | parser.add_argument( |
| 159 | "-s", "--save-dot", action="store_true", help="Save dot files." |
| 160 | ) |
| 161 | parser.add_argument("inputs", nargs="+") |
| 162 | options = parser.parse_args(argv) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 163 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 164 | input_deps = [] |
| 165 | for i in options.inputs: |
| 166 | with open(i) as handle: |
| 167 | input_deps.append(json.loads(handle.read())) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 168 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 169 | version_map = GetVersionMap(input_deps) |
| 170 | divergent_set = GetDivergentSet(version_map, len(input_deps)) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 171 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 172 | # Get all the output directories |
| 173 | all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set) |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 174 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 175 | for i in all_dirs: |
| 176 | try: |
| 177 | os.makedirs(os.path.join(options.output_dir, i)) |
| 178 | except OSError: |
| 179 | # The directory already exists. |
| 180 | pass |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 181 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 182 | for pkg in divergent_set: |
| 183 | filename = os.path.join(options.output_dir, pkg) + "." + options.format |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 184 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 185 | save_dot_filename = None |
| 186 | if options.save_dot: |
| 187 | save_dot_filename = filename + ".dot" |
Brian Harring | f90d82a | 2012-08-23 08:30:31 -0700 | [diff] [blame] | 188 | |
Alex Klein | 1699fab | 2022-09-08 08:46:06 -0600 | [diff] [blame^] | 189 | graph = BuildDependencyGraph( |
| 190 | pkg, input_deps, version_map, divergent_set |
| 191 | ) |
| 192 | lines = graph.Gen() |
| 193 | dot_helper.GenerateImage( |
| 194 | lines, filename, options.format, save_dot_filename |
| 195 | ) |