blob: b0aee96696e437a805fbfc672922e202943a0c8d [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
45 of the divergent set that transitively depend on the input node."""
46 s = set()
47 def GetClosure(name):
48 node = deps_map[name]
49 if UnversionedName(node) in divergent_set:
50 s.add(name)
51 for dep in node['rev_deps']:
52 if dep in s:
53 continue
54 GetClosure(dep)
55
56 GetClosure(full_name)
57 return s
58
59
60def GetVersionMap(input_deps):
61 """Creates the version map for the input data.
62
63 The version map maps an unversioned package name to its corresponding
64 versioned name depending on the input dependency graph.
65
66 For every package, it maps the input data index to the full name (versioned)
67 of the package in that input data. E.g.
68 map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203',
69 1:'x11-base/xorg-server-1.7.6-r8'}
70 """
71 version_map = {}
72 i = 0
73 for deps_map in input_deps:
74 for full_name, dep in deps_map.iteritems():
75 pkg = UnversionedName(dep)
76 entry = version_map.setdefault(pkg, {})
77 entry[i] = full_name
78 i += 1
79 return version_map
80
81
82def GetDivergentSet(version_map, count):
83 """Gets the set of divergent packages.
84
85 Divergent packages are those that have a different version among the input
86 dependency graphs (or missing version altogether)."""
87 divergent_set = set()
88 for pkg, value in version_map.iteritems():
89 if len(value.keys()) != count or len(set(value.values())) > 1:
90 # The package doesn't exist for at least one ot the input, or there are
91 # more than 2 versions.
92 divergent_set.add(pkg)
93 return divergent_set
94
95
96def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
97 graph = dot_helper.Graph(pkg)
98
99 # A subgraph for the divergent package we're considering. Add all its
100 # versions as a sink.
101 pkg_subgraph = graph.AddNewSubgraph('sink')
102
103 # The outer packages are those that aren't divergent but depend on a
104 # divergent package. Add them in their own subgraph, as sources.
105 outer_subgraph = graph.AddNewSubgraph('source')
106
107 emitted = set()
108 for i in xrange(0, len(input_deps)):
109 try:
110 pkg_name = version_map[pkg][i]
111 except KeyError:
112 continue
113
114 color = GetColor(i)
115
116 if pkg_name not in emitted:
117 pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
118 emitted.add(pkg_name)
119
120 # Add one subgraph per version for generally better layout.
121 subgraph = graph.AddNewSubgraph()
122
123 nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set)
124 for node_name in nodes:
125 if node_name not in emitted:
126 subgraph.AddNode(node_name, node_name, color, None)
127 emitted.add(node_name)
128
129 # Add outer packages, and all the arcs.
130 for dep in input_deps[i][node_name]['rev_deps']:
131 dep_node = input_deps[i][dep]
132 if (UnversionedName(dep_node) not in divergent_set
133 and dep not in emitted):
134 outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None)
135 emitted.add(dep)
136 graph.AddArc(dep, node_name)
137
138 return graph
139
140
141def main(argv):
142 parser = optparse.OptionParser(
143 usage='usage: %prog [options] input1 input2...')
144 parser.add_option('-f', '--format', default='svg',
145 help='Dot output format (png, svg, etc.).')
146 parser.add_option('-o', '--output-dir', default='.',
147 help='Output directory.')
148 parser.add_option('-s', '--save-dot', action='store_true',
149 help='Save dot files.')
150 options, inputs = parser.parse_args(argv)
151
152 input_deps = []
153 for i in inputs:
154 with open(i) as handle:
155 input_deps.append(json.loads(handle.read()))
156
157 version_map = GetVersionMap(input_deps)
158 divergent_set = GetDivergentSet(version_map, len(input_deps))
159
160 # Get all the output directories
161 all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)
162
163 for i in all_dirs:
164 try:
165 os.makedirs(os.path.join(options.output_dir, i))
166 except OSError:
167 # The directory already exists.
168 pass
169
170 for pkg in divergent_set:
171 filename = os.path.join(options.output_dir, pkg) + '.' + options.format
172
173 save_dot_filename = None
174 if options.save_dot:
175 save_dot_filename = filename + '.dot'
176
177 graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set)
178 lines = graph.Gen()
179 dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)