blob: 7ce6d4456fbcdc6841c15ede3546d91e6f4708f5 [file] [log] [blame]
# Copyright 2010 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates pretty dependency graphs for Chrome OS packages."""
import json
import os
import sys
from chromite.lib import commandline
from chromite.lib import dot_helper
NORMAL_COLOR = "black"
TARGET_COLOR = "red"
SEED_COLOR = "green"
CHILD_COLOR = "grey"
def GetReverseDependencyClosure(full_name, deps_map):
"""Gets the closure of the reverse dependencies of a node.
Walks the tree along all the reverse dependency paths to find all the nodes
that transitively depend on the input node.
"""
s = set()
def GetClosure(name) -> None:
s.add(name)
node = deps_map[name]
for dep in node["rev_deps"]:
if dep in s:
continue
GetClosure(dep)
GetClosure(full_name)
return s
def GetOutputBaseName(node, options):
"""Gets the basename of the output file for a node."""
return "%s_%s-%s.%s" % (
node["category"],
node["name"],
node["version"],
options.format,
)
def AddNodeToSubgraph(subgraph, node, options, color) -> None:
"""Gets the dot definition for a node."""
name = node["full_name"]
href = None
if options.link:
filename = GetOutputBaseName(node, options)
href = "%s%s" % (options.base_url, filename)
subgraph.AddNode(name, name, color, href)
def GenerateDotGraph(package, deps_map, options):
"""Generates the dot source for the dependency graph leading to a node.
The output is a list of lines.
"""
deps = GetReverseDependencyClosure(package, deps_map)
node = deps_map[package]
# Keep track of all the emitted nodes so that we don't issue multiple
# definitions
emitted = set()
graph = dot_helper.Graph(package)
# Add all the children if we want them, all of them in their own subgraph,
# as a sink. Keep the arcs outside of the subgraph though (it generates
# better layout).
children_subgraph = None
if options.children and node["deps"]:
children_subgraph = graph.AddNewSubgraph("sink")
for child in node["deps"]:
child_node = deps_map[child]
AddNodeToSubgraph(
children_subgraph, child_node, options, CHILD_COLOR
)
emitted.add(child)
graph.AddArc(package, child)
# Add the package in its own subgraph. If we didn't have children, make it
# a sink
if children_subgraph:
rank = "same"
else:
rank = "sink"
package_subgraph = graph.AddNewSubgraph(rank)
AddNodeToSubgraph(package_subgraph, node, options, TARGET_COLOR)
emitted.add(package)
# Add all the other nodes, as well as all the arcs.
for dep in deps:
dep_node = deps_map[dep]
if not dep in emitted:
color = NORMAL_COLOR
if dep_node["action"] == "seed":
color = SEED_COLOR
AddNodeToSubgraph(graph, dep_node, options, color)
for j in dep_node["rev_deps"]:
graph.AddArc(j, dep)
return graph.Gen()
def GenerateImages(data, options) -> None:
"""Generate the output images for all the nodes in the input."""
deps_map = json.loads(data)
for package in deps_map:
lines = GenerateDotGraph(package, deps_map, options)
filename = os.path.join(
options.output_dir, GetOutputBaseName(deps_map[package], options)
)
save_dot_filename = None
if options.save_dot:
save_dot_filename = filename + ".dot"
dot_helper.GenerateImage(
lines, filename, options.format, save_dot_filename
)
def GetParser():
"""Return a command line parser."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument(
"-f",
"--format",
default="svg",
help="Dot output format (png, svg, etc.).",
)
parser.add_argument(
"-o", "--output-dir", default=".", help="Output directory."
)
parser.add_argument(
"-c", "--children", action="store_true", help="Also add children."
)
parser.add_argument(
"-l", "--link", action="store_true", help="Embed links."
)
parser.add_argument(
"-b", "--base-url", default="", help="Base url for links."
)
parser.add_argument(
"-s", "--save-dot", action="store_true", help="Save dot files."
)
parser.add_argument("inputs", nargs="*", help="Chromium OS package lists")
return parser
def main(argv) -> None:
parser = GetParser()
options = parser.parse_args(argv)
options.Freeze()
try:
os.makedirs(options.output_dir)
except OSError:
# The directory already exists.
pass
if not options.inputs:
GenerateImages(sys.stdin.read(), options)
else:
for i in options.inputs:
with open(i, encoding="utf-8") as handle:
GenerateImages(handle.read(), options)