blob: ecca47d1c0462bbb5ef4ff7ad9daaa046978d885 [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 pretty dependency graphs for Chrome OS packages."""
6
7import json
Brian Harringf90d82a2012-08-23 08:30:31 -07008import os
9import sys
10
Mike Frysingerd6d39652016-09-01 00:27:59 -040011from chromite.lib import commandline
Brian Harringf90d82a2012-08-23 08:30:31 -070012from chromite.lib import dot_helper
13
14
15NORMAL_COLOR = 'black'
16TARGET_COLOR = 'red'
17SEED_COLOR = 'green'
18CHILD_COLOR = 'grey'
19
20
21def GetReverseDependencyClosure(full_name, deps_map):
22 """Gets the closure of the reverse dependencies of a node.
23
24 Walks the tree along all the reverse dependency paths to find all the nodes
Mike Frysinger02e1e072013-11-10 22:11:34 -050025 that transitively depend on the input node.
26 """
Brian Harringf90d82a2012-08-23 08:30:31 -070027 s = set()
28 def GetClosure(name):
29 s.add(name)
30 node = deps_map[name]
31 for dep in node['rev_deps']:
32 if dep in s:
33 continue
34 GetClosure(dep)
35
36 GetClosure(full_name)
37 return s
38
39
40def GetOutputBaseName(node, options):
41 """Gets the basename of the output file for a node."""
42 return '%s_%s-%s.%s' % (node['category'], node['name'], node['version'],
43 options.format)
44
45
46def AddNodeToSubgraph(subgraph, node, options, color):
47 """Gets the dot definition for a node."""
48 name = node['full_name']
49 href = None
50 if options.link:
51 filename = GetOutputBaseName(node, options)
52 href = '%s%s' % (options.base_url, filename)
53 subgraph.AddNode(name, name, color, href)
54
55
56
57def GenerateDotGraph(package, deps_map, options):
58 """Generates the dot source for the dependency graph leading to a node.
59
Mike Frysinger02e1e072013-11-10 22:11:34 -050060 The output is a list of lines.
61 """
Brian Harringf90d82a2012-08-23 08:30:31 -070062 deps = GetReverseDependencyClosure(package, deps_map)
63 node = deps_map[package]
64
65 # Keep track of all the emitted nodes so that we don't issue multiple
66 # definitions
67 emitted = set()
68
69 graph = dot_helper.Graph(package)
70
71 # Add all the children if we want them, all of them in their own subgraph,
72 # as a sink. Keep the arcs outside of the subgraph though (it generates
73 # better layout).
74 children_subgraph = None
75 if options.children and node['deps']:
76 children_subgraph = graph.AddNewSubgraph('sink')
77 for child in node['deps']:
78 child_node = deps_map[child]
79 AddNodeToSubgraph(children_subgraph, child_node, options, CHILD_COLOR)
80 emitted.add(child)
81 graph.AddArc(package, child)
82
83 # Add the package in its own subgraph. If we didn't have children, make it
84 # a sink
85 if children_subgraph:
86 rank = 'same'
87 else:
88 rank = 'sink'
89 package_subgraph = graph.AddNewSubgraph(rank)
90 AddNodeToSubgraph(package_subgraph, node, options, TARGET_COLOR)
91 emitted.add(package)
92
93 # Add all the other nodes, as well as all the arcs.
94 for dep in deps:
95 dep_node = deps_map[dep]
96 if not dep in emitted:
97 color = NORMAL_COLOR
98 if dep_node['action'] == 'seed':
99 color = SEED_COLOR
100 AddNodeToSubgraph(graph, dep_node, options, color)
101 for j in dep_node['rev_deps']:
102 graph.AddArc(j, dep)
103
104 return graph.Gen()
105
106
107def GenerateImages(data, options):
108 """Generate the output images for all the nodes in the input."""
109 deps_map = json.loads(data)
110
111 for package in deps_map:
112 lines = GenerateDotGraph(package, deps_map, options)
113
114 filename = os.path.join(options.output_dir,
115 GetOutputBaseName(deps_map[package], options))
116
117 save_dot_filename = None
118 if options.save_dot:
119 save_dot_filename = filename + '.dot'
120
121 dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)
122
123
Mike Frysingerd6d39652016-09-01 00:27:59 -0400124def GetParser():
125 """Return a command line parser."""
126 parser = commandline.ArgumentParser(description=__doc__)
127 parser.add_argument('-f', '--format', default='svg',
128 help='Dot output format (png, svg, etc.).')
129 parser.add_argument('-o', '--output-dir', default='.',
130 help='Output directory.')
131 parser.add_argument('-c', '--children', action='store_true',
132 help='Also add children.')
133 parser.add_argument('-l', '--link', action='store_true',
134 help='Embed links.')
135 parser.add_argument('-b', '--base-url', default='',
136 help='Base url for links.')
137 parser.add_argument('-s', '--save-dot', action='store_true',
138 help='Save dot files.')
139 parser.add_argument('inputs', nargs='*',
140 help='Chromium OS package lists')
141 return parser
142
143
Brian Harringf90d82a2012-08-23 08:30:31 -0700144def main(argv):
Mike Frysingerd6d39652016-09-01 00:27:59 -0400145 parser = GetParser()
146 options = parser.parse_args(argv)
147 options.Freeze()
Brian Harringf90d82a2012-08-23 08:30:31 -0700148
149 try:
150 os.makedirs(options.output_dir)
151 except OSError:
152 # The directory already exists.
153 pass
154
Mike Frysingerd6d39652016-09-01 00:27:59 -0400155 if not options.inputs:
Brian Harringf90d82a2012-08-23 08:30:31 -0700156 GenerateImages(sys.stdin.read(), options)
157 else:
Mike Frysingerd6d39652016-09-01 00:27:59 -0400158 for i in options.inputs:
Brian Harringf90d82a2012-08-23 08:30:31 -0700159 with open(i) as handle:
160 GenerateImages(handle.read(), options)