blob: 855a9afed1b77510bfc7dd0b55ff2a42b5212825 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2010 The ChromiumOS Authors
Brian Harringf90d82a2012-08-23 08:30:31 -07002# 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
7As an input it takes 2 or more dependency graphs output from cros_extract_deps
8and it finds all divergent packages (packages whose versions differ between
9some of these dependency graphs) and outputs graphs that trace the divergence
10in the dependency trees until common packages are found.
11"""
12
13import json
Brian Harringf90d82a2012-08-23 08:30:31 -070014import os
15
Mike Frysingera9436d12015-06-04 01:49:41 -040016from chromite.lib import commandline
Brian Harringf90d82a2012-08-23 08:30:31 -070017from chromite.lib import dot_helper
18
Mike Frysinger807d8282022-04-28 22:45:17 -040019
Alex Klein1699fab2022-09-08 08:46:06 -060020NORMAL_COLOR = "black"
21BASE_COLORS = ["red", "green", "blue"]
Brian Harringf90d82a2012-08-23 08:30:31 -070022
23
24def UnversionedName(dep):
Alex Klein1699fab2022-09-08 08:46:06 -060025 """Returns the name of the package, omitting the version."""
26 return "%s/%s" % (dep["category"], dep["name"])
Brian Harringf90d82a2012-08-23 08:30:31 -070027
28
29def GetColor(index):
Alex Klein1699fab2022-09-08 08:46:06 -060030 """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 Harringf90d82a2012-08-23 08:30:31 -070040
41
42def GetReverseDependencyClosure(full_name, deps_map, divergent_set):
Alex Klein1699fab2022-09-08 08:46:06 -060043 """Gets the closure of the reverse dependencies of a node.
Brian Harringf90d82a2012-08-23 08:30:31 -070044
Alex Klein1699fab2022-09-08 08:46:06 -060045 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 Harringf90d82a2012-08-23 08:30:31 -070049
Alex Klein1699fab2022-09-08 08:46:06 -060050 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 Harringf90d82a2012-08-23 08:30:31 -070061
62
63def GetVersionMap(input_deps):
Alex Klein1699fab2022-09-08 08:46:06 -060064 """Creates the version map for the input data.
Brian Harringf90d82a2012-08-23 08:30:31 -070065
Alex Klein1699fab2022-09-08 08:46:06 -060066 The version map maps an unversioned package name to its corresponding
67 versioned name depending on the input dependency graph.
Brian Harringf90d82a2012-08-23 08:30:31 -070068
Alex Klein1699fab2022-09-08 08:46:06 -060069 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 Harringf90d82a2012-08-23 08:30:31 -070083
84
85def GetDivergentSet(version_map, count):
Alex Klein1699fab2022-09-08 08:46:06 -060086 """Gets the set of divergent packages.
Brian Harringf90d82a2012-08-23 08:30:31 -070087
Alex Klein1699fab2022-09-08 08:46:06 -060088 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 Harringf90d82a2012-08-23 08:30:31 -070098
99
100def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
Alex Klein1699fab2022-09-08 08:46:06 -0600101 graph = dot_helper.Graph(pkg)
Brian Harringf90d82a2012-08-23 08:30:31 -0700102
Alex Klein1699fab2022-09-08 08:46:06 -0600103 # A subgraph for the divergent package we're considering. Add all its
104 # versions as a sink.
105 pkg_subgraph = graph.AddNewSubgraph("sink")
Brian Harringf90d82a2012-08-23 08:30:31 -0700106
Alex Klein1699fab2022-09-08 08:46:06 -0600107 # 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 Harringf90d82a2012-08-23 08:30:31 -0700110
Alex Klein1699fab2022-09-08 08:46:06 -0600111 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 Harringf90d82a2012-08-23 08:30:31 -0700117
Alex Klein1699fab2022-09-08 08:46:06 -0600118 color = GetColor(i)
Brian Harringf90d82a2012-08-23 08:30:31 -0700119
Alex Klein1699fab2022-09-08 08:46:06 -0600120 if pkg_name not in emitted:
121 pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
122 emitted.add(pkg_name)
Brian Harringf90d82a2012-08-23 08:30:31 -0700123
Alex Klein1699fab2022-09-08 08:46:06 -0600124 # Add one subgraph per version for generally better layout.
125 subgraph = graph.AddNewSubgraph()
Brian Harringf90d82a2012-08-23 08:30:31 -0700126
Alex Klein1699fab2022-09-08 08:46:06 -0600127 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 Harringf90d82a2012-08-23 08:30:31 -0700132
Alex Klein1699fab2022-09-08 08:46:06 -0600133 # 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 Harringf90d82a2012-08-23 08:30:31 -0700143
Alex Klein1699fab2022-09-08 08:46:06 -0600144 return graph
Brian Harringf90d82a2012-08-23 08:30:31 -0700145
146
147def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600148 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 Harringf90d82a2012-08-23 08:30:31 -0700163
Alex Klein1699fab2022-09-08 08:46:06 -0600164 input_deps = []
165 for i in options.inputs:
Mike Frysinger31fdddd2023-02-24 15:50:55 -0500166 with open(i, "rb") as handle:
167 input_deps.append(json.load(handle))
Brian Harringf90d82a2012-08-23 08:30:31 -0700168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 version_map = GetVersionMap(input_deps)
170 divergent_set = GetDivergentSet(version_map, len(input_deps))
Brian Harringf90d82a2012-08-23 08:30:31 -0700171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 # Get all the output directories
173 all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)
Brian Harringf90d82a2012-08-23 08:30:31 -0700174
Alex Klein1699fab2022-09-08 08:46:06 -0600175 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 Harringf90d82a2012-08-23 08:30:31 -0700181
Alex Klein1699fab2022-09-08 08:46:06 -0600182 for pkg in divergent_set:
183 filename = os.path.join(options.output_dir, pkg) + "." + options.format
Brian Harringf90d82a2012-08-23 08:30:31 -0700184
Alex Klein1699fab2022-09-08 08:46:06 -0600185 save_dot_filename = None
186 if options.save_dot:
187 save_dot_filename = filename + ".dot"
Brian Harringf90d82a2012-08-23 08:30:31 -0700188
Alex Klein1699fab2022-09-08 08:46:06 -0600189 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 )