blob: 8633f941007fa1bed3df5efdddd9b7cbfa948699 [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
14import optparse
15import os
16
17from 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
36 for bit in xrange(0, 24):
37 chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3)
38 return "#%02x%02x%02x" % tuple(chars)
39
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:
75 for full_name, dep in deps_map.iteritems():
76 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()
90 for pkg, value in version_map.iteritems():
91 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()
110 for i in xrange(0, len(input_deps)):
111 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
125 nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set)
126 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.
132 for dep in input_deps[i][node_name]['rev_deps']:
133 dep_node = input_deps[i][dep]
134 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):
144 parser = optparse.OptionParser(
145 usage='usage: %prog [options] input1 input2...')
146 parser.add_option('-f', '--format', default='svg',
147 help='Dot output format (png, svg, etc.).')
148 parser.add_option('-o', '--output-dir', default='.',
149 help='Output directory.')
150 parser.add_option('-s', '--save-dot', action='store_true',
151 help='Save dot files.')
152 options, inputs = parser.parse_args(argv)
153
154 input_deps = []
155 for i in inputs:
156 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)