blob: d2e474977642faa3810d6dddebc806d31396bef5 [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
Mike Frysinger383367e2014-09-16 15:06:17 -040013from __future__ import print_function
14
Brian Harringf90d82a2012-08-23 08:30:31 -070015import json
16import optparse
17import os
18
19from chromite.lib import dot_helper
20
21NORMAL_COLOR = 'black'
22BASE_COLORS = ['red', 'green', 'blue']
23
24
25def UnversionedName(dep):
26 """Returns the name of the package, omitting the version."""
27 return '%s/%s' % (dep['category'], dep['name'])
28
29
30def GetColor(index):
31 """Maps index to a color."""
32 try:
33 return BASE_COLORS[index]
34 except IndexError:
35 # Generate a color by splicing the bits to generate high contrast colors
36 index -= len(BASE_COLORS) - 1
37 chars = [0] * 3
38 for bit in xrange(0, 24):
39 chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3)
40 return "#%02x%02x%02x" % tuple(chars)
41
42
43def GetReverseDependencyClosure(full_name, deps_map, divergent_set):
44 """Gets the closure of the reverse dependencies of a node.
45
46 Walks the tree along all the reverse dependency paths to find all the nodes
Mike Frysinger02e1e072013-11-10 22:11:34 -050047 of the divergent set that transitively depend on the input node.
48 """
Brian Harringf90d82a2012-08-23 08:30:31 -070049 s = set()
50 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
61
62
63def GetVersionMap(input_deps):
64 """Creates the version map for the input data.
65
66 The version map maps an unversioned package name to its corresponding
67 versioned name depending on the input dependency graph.
68
69 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.iteritems():
78 pkg = UnversionedName(dep)
79 entry = version_map.setdefault(pkg, {})
80 entry[i] = full_name
81 i += 1
82 return version_map
83
84
85def GetDivergentSet(version_map, count):
86 """Gets the set of divergent packages.
87
88 Divergent packages are those that have a different version among the input
Mike Frysinger02e1e072013-11-10 22:11:34 -050089 dependency graphs (or missing version altogether).
90 """
Brian Harringf90d82a2012-08-23 08:30:31 -070091 divergent_set = set()
92 for pkg, value in version_map.iteritems():
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
98
99
100def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
101 graph = dot_helper.Graph(pkg)
102
103 # A subgraph for the divergent package we're considering. Add all its
104 # versions as a sink.
105 pkg_subgraph = graph.AddNewSubgraph('sink')
106
107 # 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')
110
111 emitted = set()
112 for i in xrange(0, len(input_deps)):
113 try:
114 pkg_name = version_map[pkg][i]
115 except KeyError:
116 continue
117
118 color = GetColor(i)
119
120 if pkg_name not in emitted:
121 pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
122 emitted.add(pkg_name)
123
124 # Add one subgraph per version for generally better layout.
125 subgraph = graph.AddNewSubgraph()
126
127 nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], 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)
132
133 # Add outer packages, and all the arcs.
134 for dep in input_deps[i][node_name]['rev_deps']:
135 dep_node = input_deps[i][dep]
136 if (UnversionedName(dep_node) not in divergent_set
137 and dep not in emitted):
138 outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None)
139 emitted.add(dep)
140 graph.AddArc(dep, node_name)
141
142 return graph
143
144
145def main(argv):
146 parser = optparse.OptionParser(
147 usage='usage: %prog [options] input1 input2...')
148 parser.add_option('-f', '--format', default='svg',
149 help='Dot output format (png, svg, etc.).')
150 parser.add_option('-o', '--output-dir', default='.',
151 help='Output directory.')
152 parser.add_option('-s', '--save-dot', action='store_true',
153 help='Save dot files.')
154 options, inputs = parser.parse_args(argv)
155
156 input_deps = []
157 for i in inputs:
158 with open(i) as handle:
159 input_deps.append(json.loads(handle.read()))
160
161 version_map = GetVersionMap(input_deps)
162 divergent_set = GetDivergentSet(version_map, len(input_deps))
163
164 # Get all the output directories
165 all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)
166
167 for i in all_dirs:
168 try:
169 os.makedirs(os.path.join(options.output_dir, i))
170 except OSError:
171 # The directory already exists.
172 pass
173
174 for pkg in divergent_set:
175 filename = os.path.join(options.output_dir, pkg) + '.' + options.format
176
177 save_dot_filename = None
178 if options.save_dot:
179 save_dot_filename = filename + '.dot'
180
181 graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set)
182 lines = graph.Gen()
183 dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)