blob: b0aee96696e437a805fbfc672922e202943a0c8d [file] [log] [blame]
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates dependency graph diffs.
As an input it takes 2 or more dependency graphs output from cros_extract_deps
and it finds all divergent packages (packages whose versions differ between
some of these dependency graphs) and outputs graphs that trace the divergence
in the dependency trees until common packages are found.
import json
import optparse
import os
from chromite.lib import dot_helper
NORMAL_COLOR = 'black'
BASE_COLORS = ['red', 'green', 'blue']
def UnversionedName(dep):
"""Returns the name of the package, omitting the version."""
return '%s/%s' % (dep['category'], dep['name'])
def GetColor(index):
"""Maps index to a color."""
return BASE_COLORS[index]
except IndexError:
# Generate a color by splicing the bits to generate high contrast colors
index -= len(BASE_COLORS) - 1
chars = [0] * 3
for bit in xrange(0, 24):
chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3)
return "#%02x%02x%02x" % tuple(chars)
def GetReverseDependencyClosure(full_name, deps_map, divergent_set):
"""Gets the closure of the reverse dependencies of a node.
Walks the tree along all the reverse dependency paths to find all the nodes
of the divergent set that transitively depend on the input node."""
s = set()
def GetClosure(name):
node = deps_map[name]
if UnversionedName(node) in divergent_set:
for dep in node['rev_deps']:
if dep in s:
return s
def GetVersionMap(input_deps):
"""Creates the version map for the input data.
The version map maps an unversioned package name to its corresponding
versioned name depending on the input dependency graph.
For every package, it maps the input data index to the full name (versioned)
of the package in that input data. E.g.
map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203',
version_map = {}
i = 0
for deps_map in input_deps:
for full_name, dep in deps_map.iteritems():
pkg = UnversionedName(dep)
entry = version_map.setdefault(pkg, {})
entry[i] = full_name
i += 1
return version_map
def GetDivergentSet(version_map, count):
"""Gets the set of divergent packages.
Divergent packages are those that have a different version among the input
dependency graphs (or missing version altogether)."""
divergent_set = set()
for pkg, value in version_map.iteritems():
if len(value.keys()) != count or len(set(value.values())) > 1:
# The package doesn't exist for at least one ot the input, or there are
# more than 2 versions.
return divergent_set
def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
graph = dot_helper.Graph(pkg)
# A subgraph for the divergent package we're considering. Add all its
# versions as a sink.
pkg_subgraph = graph.AddNewSubgraph('sink')
# The outer packages are those that aren't divergent but depend on a
# divergent package. Add them in their own subgraph, as sources.
outer_subgraph = graph.AddNewSubgraph('source')
emitted = set()
for i in xrange(0, len(input_deps)):
pkg_name = version_map[pkg][i]
except KeyError:
color = GetColor(i)
if pkg_name not in emitted:
pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
# Add one subgraph per version for generally better layout.
subgraph = graph.AddNewSubgraph()
nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set)
for node_name in nodes:
if node_name not in emitted:
subgraph.AddNode(node_name, node_name, color, None)
# Add outer packages, and all the arcs.
for dep in input_deps[i][node_name]['rev_deps']:
dep_node = input_deps[i][dep]
if (UnversionedName(dep_node) not in divergent_set
and dep not in emitted):
outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None)
graph.AddArc(dep, node_name)
return graph
def main(argv):
parser = optparse.OptionParser(
usage='usage: %prog [options] input1 input2...')
parser.add_option('-f', '--format', default='svg',
help='Dot output format (png, svg, etc.).')
parser.add_option('-o', '--output-dir', default='.',
help='Output directory.')
parser.add_option('-s', '--save-dot', action='store_true',
help='Save dot files.')
options, inputs = parser.parse_args(argv)
input_deps = []
for i in inputs:
with open(i) as handle:
version_map = GetVersionMap(input_deps)
divergent_set = GetDivergentSet(version_map, len(input_deps))
# Get all the output directories
all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)
for i in all_dirs:
os.makedirs(os.path.join(options.output_dir, i))
except OSError:
# The directory already exists.
for pkg in divergent_set:
filename = os.path.join(options.output_dir, pkg) + '.' + options.format
save_dot_filename = None
if options.save_dot:
save_dot_filename = filename + '.dot'
graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set)
lines = graph.Gen()
dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)