blob: b68d65f8ca37c89650e2aa0c0cc35864308dfef8 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Brian Harringf90d82a2012-08-23 08:30:31 -07002# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Generates dependency graph diffs.
7
8As an input it takes 2 or more dependency graphs output from cros_extract_deps
9and it finds all divergent packages (packages whose versions differ between
10some of these dependency graphs) and outputs graphs that trace the divergence
11in the dependency trees until common packages are found.
12"""
13
Mike Frysinger383367e2014-09-16 15:06:17 -040014from __future__ import print_function
15
Brian Harringf90d82a2012-08-23 08:30:31 -070016import json
Brian Harringf90d82a2012-08-23 08:30:31 -070017import os
18
Mike Frysingera9436d12015-06-04 01:49:41 -040019from chromite.lib import commandline
Brian Harringf90d82a2012-08-23 08:30:31 -070020from chromite.lib import dot_helper
21
22NORMAL_COLOR = 'black'
23BASE_COLORS = ['red', 'green', 'blue']
24
25
26def UnversionedName(dep):
27 """Returns the name of the package, omitting the version."""
28 return '%s/%s' % (dep['category'], dep['name'])
29
30
31def GetColor(index):
32 """Maps index to a color."""
33 try:
34 return BASE_COLORS[index]
35 except IndexError:
36 # Generate a color by splicing the bits to generate high contrast colors
37 index -= len(BASE_COLORS) - 1
38 chars = [0] * 3
Mike Frysinger79cca962019-06-13 15:26:53 -040039 for bit in range(0, 24):
Brian Harringf90d82a2012-08-23 08:30:31 -070040 chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3)
Mike Frysinger80de5012019-08-01 14:10:53 -040041 return '#%02x%02x%02x' % tuple(chars)
Brian Harringf90d82a2012-08-23 08:30:31 -070042
43
44def GetReverseDependencyClosure(full_name, deps_map, divergent_set):
45 """Gets the closure of the reverse dependencies of a node.
46
47 Walks the tree along all the reverse dependency paths to find all the nodes
Mike Frysinger02e1e072013-11-10 22:11:34 -050048 of the divergent set that transitively depend on the input node.
49 """
Brian Harringf90d82a2012-08-23 08:30:31 -070050 s = set()
51 def GetClosure(name):
52 node = deps_map[name]
53 if UnversionedName(node) in divergent_set:
54 s.add(name)
55 for dep in node['rev_deps']:
56 if dep in s:
57 continue
58 GetClosure(dep)
59
60 GetClosure(full_name)
61 return s
62
63
64def GetVersionMap(input_deps):
65 """Creates the version map for the input data.
66
67 The version map maps an unversioned package name to its corresponding
68 versioned name depending on the input dependency graph.
69
70 For every package, it maps the input data index to the full name (versioned)
71 of the package in that input data. E.g.
72 map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203',
73 1:'x11-base/xorg-server-1.7.6-r8'}
74 """
75 version_map = {}
76 i = 0
77 for deps_map in input_deps:
Mike Frysinger0bdbc102019-06-13 15:27:29 -040078 for full_name, dep in deps_map.items():
Brian Harringf90d82a2012-08-23 08:30:31 -070079 pkg = UnversionedName(dep)
80 entry = version_map.setdefault(pkg, {})
81 entry[i] = full_name
82 i += 1
83 return version_map
84
85
86def GetDivergentSet(version_map, count):
87 """Gets the set of divergent packages.
88
89 Divergent packages are those that have a different version among the input
Mike Frysinger02e1e072013-11-10 22:11:34 -050090 dependency graphs (or missing version altogether).
91 """
Brian Harringf90d82a2012-08-23 08:30:31 -070092 divergent_set = set()
Mike Frysinger0bdbc102019-06-13 15:27:29 -040093 for pkg, value in version_map.items():
Brian Harringf90d82a2012-08-23 08:30:31 -070094 if len(value.keys()) != count or len(set(value.values())) > 1:
95 # The package doesn't exist for at least one ot the input, or there are
96 # more than 2 versions.
97 divergent_set.add(pkg)
98 return divergent_set
99
100
101def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
102 graph = dot_helper.Graph(pkg)
103
104 # A subgraph for the divergent package we're considering. Add all its
105 # versions as a sink.
106 pkg_subgraph = graph.AddNewSubgraph('sink')
107
108 # The outer packages are those that aren't divergent but depend on a
109 # divergent package. Add them in their own subgraph, as sources.
110 outer_subgraph = graph.AddNewSubgraph('source')
111
112 emitted = set()
Mike Frysinger79cca962019-06-13 15:26:53 -0400113 for i in range(0, len(input_deps)):
Brian Harringf90d82a2012-08-23 08:30:31 -0700114 try:
115 pkg_name = version_map[pkg][i]
116 except KeyError:
117 continue
118
119 color = GetColor(i)
120
121 if pkg_name not in emitted:
122 pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
123 emitted.add(pkg_name)
124
125 # Add one subgraph per version for generally better layout.
126 subgraph = graph.AddNewSubgraph()
127
128 nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set)
129 for node_name in nodes:
130 if node_name not in emitted:
131 subgraph.AddNode(node_name, node_name, color, None)
132 emitted.add(node_name)
133
134 # Add outer packages, and all the arcs.
135 for dep in input_deps[i][node_name]['rev_deps']:
136 dep_node = input_deps[i][dep]
137 if (UnversionedName(dep_node) not in divergent_set
138 and dep not in emitted):
139 outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None)
140 emitted.add(dep)
141 graph.AddArc(dep, node_name)
142
143 return graph
144
145
146def main(argv):
Mike Frysingera9436d12015-06-04 01:49:41 -0400147 parser = commandline.ArgumentParser(description=__doc__)
148 parser.add_argument('-f', '--format', default='svg',
149 help='Dot output format (png, svg, etc.).')
150 parser.add_argument('-o', '--output-dir', default='.',
151 help='Output directory.')
152 parser.add_argument('-s', '--save-dot', action='store_true',
153 help='Save dot files.')
154 parser.add_argument('inputs', nargs='+')
155 options = parser.parse_args(argv)
Brian Harringf90d82a2012-08-23 08:30:31 -0700156
157 input_deps = []
Mike Frysingera9436d12015-06-04 01:49:41 -0400158 for i in options.inputs:
Brian Harringf90d82a2012-08-23 08:30:31 -0700159 with open(i) as handle:
160 input_deps.append(json.loads(handle.read()))
161
162 version_map = GetVersionMap(input_deps)
163 divergent_set = GetDivergentSet(version_map, len(input_deps))
164
165 # Get all the output directories
166 all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)
167
168 for i in all_dirs:
169 try:
170 os.makedirs(os.path.join(options.output_dir, i))
171 except OSError:
172 # The directory already exists.
173 pass
174
175 for pkg in divergent_set:
176 filename = os.path.join(options.output_dir, pkg) + '.' + options.format
177
178 save_dot_filename = None
179 if options.save_dot:
180 save_dot_filename = filename + '.dot'
181
182 graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set)
183 lines = graph.Gen()
184 dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)