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