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