blob: edb248727ccbf48389ffb3b4785eeea2e4511fab [file] [log] [blame]
Brian Harringf90d82a2012-08-23 08:30:31 -07001# 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
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
Brian Harringf90d82a2012-08-23 08:30:31 -070020NORMAL_COLOR = 'black'
21BASE_COLORS = ['red', 'green', 'blue']
22
23
24def UnversionedName(dep):
25 """Returns the name of the package, omitting the version."""
26 return '%s/%s' % (dep['category'], dep['name'])
27
28
29def GetColor(index):
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
Mike Frysinger79cca962019-06-13 15:26:53 -040037 for bit in range(0, 24):
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040038 chars[bit % 3] |= ((index >> bit) & 0x1) << (7 - bit // 3)
Mike Frysinger80de5012019-08-01 14:10:53 -040039 return '#%02x%02x%02x' % tuple(chars)
Brian Harringf90d82a2012-08-23 08:30:31 -070040
41
42def GetReverseDependencyClosure(full_name, deps_map, divergent_set):
43 """Gets the closure of the reverse dependencies of a node.
44
45 Walks the tree along all the reverse dependency paths to find all the nodes
Mike Frysinger02e1e072013-11-10 22:11:34 -050046 of the divergent set that transitively depend on the input node.
47 """
Brian Harringf90d82a2012-08-23 08:30:31 -070048 s = set()
49 def GetClosure(name):
50 node = deps_map[name]
51 if UnversionedName(node) in divergent_set:
52 s.add(name)
53 for dep in node['rev_deps']:
54 if dep in s:
55 continue
56 GetClosure(dep)
57
58 GetClosure(full_name)
59 return s
60
61
62def GetVersionMap(input_deps):
63 """Creates the version map for the input data.
64
65 The version map maps an unversioned package name to its corresponding
66 versioned name depending on the input dependency graph.
67
68 For every package, it maps the input data index to the full name (versioned)
69 of the package in that input data. E.g.
70 map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203',
71 1:'x11-base/xorg-server-1.7.6-r8'}
72 """
73 version_map = {}
74 i = 0
75 for deps_map in input_deps:
Mike Frysinger0bdbc102019-06-13 15:27:29 -040076 for full_name, dep in deps_map.items():
Brian Harringf90d82a2012-08-23 08:30:31 -070077 pkg = UnversionedName(dep)
78 entry = version_map.setdefault(pkg, {})
79 entry[i] = full_name
80 i += 1
81 return version_map
82
83
84def GetDivergentSet(version_map, count):
85 """Gets the set of divergent packages.
86
87 Divergent packages are those that have a different version among the input
Mike Frysinger02e1e072013-11-10 22:11:34 -050088 dependency graphs (or missing version altogether).
89 """
Brian Harringf90d82a2012-08-23 08:30:31 -070090 divergent_set = set()
Mike Frysinger0bdbc102019-06-13 15:27:29 -040091 for pkg, value in version_map.items():
Brian Harringf90d82a2012-08-23 08:30:31 -070092 if len(value.keys()) != count or len(set(value.values())) > 1:
93 # The package doesn't exist for at least one ot the input, or there are
94 # more than 2 versions.
95 divergent_set.add(pkg)
96 return divergent_set
97
98
99def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
100 graph = dot_helper.Graph(pkg)
101
102 # A subgraph for the divergent package we're considering. Add all its
103 # versions as a sink.
104 pkg_subgraph = graph.AddNewSubgraph('sink')
105
106 # The outer packages are those that aren't divergent but depend on a
107 # divergent package. Add them in their own subgraph, as sources.
108 outer_subgraph = graph.AddNewSubgraph('source')
109
110 emitted = set()
Mike Frysinger8017a6a2019-10-20 17:40:29 -0400111 for i, input_dep in enumerate(input_deps):
Brian Harringf90d82a2012-08-23 08:30:31 -0700112 try:
113 pkg_name = version_map[pkg][i]
114 except KeyError:
115 continue
116
117 color = GetColor(i)
118
119 if pkg_name not in emitted:
120 pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
121 emitted.add(pkg_name)
122
123 # Add one subgraph per version for generally better layout.
124 subgraph = graph.AddNewSubgraph()
125
Mike Frysinger8017a6a2019-10-20 17:40:29 -0400126 nodes = GetReverseDependencyClosure(pkg_name, input_dep, divergent_set)
Brian Harringf90d82a2012-08-23 08:30:31 -0700127 for node_name in nodes:
128 if node_name not in emitted:
129 subgraph.AddNode(node_name, node_name, color, None)
130 emitted.add(node_name)
131
132 # Add outer packages, and all the arcs.
Mike Frysinger8017a6a2019-10-20 17:40:29 -0400133 for dep in input_dep[node_name]['rev_deps']:
134 dep_node = input_dep[dep]
Brian Harringf90d82a2012-08-23 08:30:31 -0700135 if (UnversionedName(dep_node) not in divergent_set
136 and dep not in emitted):
137 outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None)
138 emitted.add(dep)
139 graph.AddArc(dep, node_name)
140
141 return graph
142
143
144def main(argv):
Mike Frysingera9436d12015-06-04 01:49:41 -0400145 parser = commandline.ArgumentParser(description=__doc__)
146 parser.add_argument('-f', '--format', default='svg',
147 help='Dot output format (png, svg, etc.).')
148 parser.add_argument('-o', '--output-dir', default='.',
149 help='Output directory.')
150 parser.add_argument('-s', '--save-dot', action='store_true',
151 help='Save dot files.')
152 parser.add_argument('inputs', nargs='+')
153 options = parser.parse_args(argv)
Brian Harringf90d82a2012-08-23 08:30:31 -0700154
155 input_deps = []
Mike Frysingera9436d12015-06-04 01:49:41 -0400156 for i in options.inputs:
Brian Harringf90d82a2012-08-23 08:30:31 -0700157 with open(i) as handle:
158 input_deps.append(json.loads(handle.read()))
159
160 version_map = GetVersionMap(input_deps)
161 divergent_set = GetDivergentSet(version_map, len(input_deps))
162
163 # Get all the output directories
164 all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)
165
166 for i in all_dirs:
167 try:
168 os.makedirs(os.path.join(options.output_dir, i))
169 except OSError:
170 # The directory already exists.
171 pass
172
173 for pkg in divergent_set:
174 filename = os.path.join(options.output_dir, pkg) + '.' + options.format
175
176 save_dot_filename = None
177 if options.save_dot:
178 save_dot_filename = filename + '.dot'
179
180 graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set)
181 lines = graph.Gen()
182 dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)