blob: 14e37e7ac6be9c971d59a218bfe47a7a40cff959 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2010 The ChromiumOS Authors
Brian Harringf90d82a2012-08-23 08:30:31 -07002# 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
Alex Klein1699fab2022-09-08 08:46:06 -060015NORMAL_COLOR = "black"
16TARGET_COLOR = "red"
17SEED_COLOR = "green"
18CHILD_COLOR = "grey"
Brian Harringf90d82a2012-08-23 08:30:31 -070019
20
21def GetReverseDependencyClosure(full_name, deps_map):
Alex Klein1699fab2022-09-08 08:46:06 -060022 """Gets the closure of the reverse dependencies of a node.
Brian Harringf90d82a2012-08-23 08:30:31 -070023
Alex Klein1699fab2022-09-08 08:46:06 -060024 Walks the tree along all the reverse dependency paths to find all the nodes
25 that transitively depend on the input node.
26 """
27 s = set()
Brian Harringf90d82a2012-08-23 08:30:31 -070028
Alex Klein1699fab2022-09-08 08:46:06 -060029 def GetClosure(name):
30 s.add(name)
31 node = deps_map[name]
32 for dep in node["rev_deps"]:
33 if dep in s:
34 continue
35 GetClosure(dep)
36
37 GetClosure(full_name)
38 return s
Brian Harringf90d82a2012-08-23 08:30:31 -070039
40
41def GetOutputBaseName(node, options):
Alex Klein1699fab2022-09-08 08:46:06 -060042 """Gets the basename of the output file for a node."""
43 return "%s_%s-%s.%s" % (
44 node["category"],
45 node["name"],
46 node["version"],
47 options.format,
48 )
Brian Harringf90d82a2012-08-23 08:30:31 -070049
50
51def AddNodeToSubgraph(subgraph, node, options, color):
Alex Klein1699fab2022-09-08 08:46:06 -060052 """Gets the dot definition for a node."""
53 name = node["full_name"]
54 href = None
55 if options.link:
56 filename = GetOutputBaseName(node, options)
57 href = "%s%s" % (options.base_url, filename)
58 subgraph.AddNode(name, name, color, href)
Brian Harringf90d82a2012-08-23 08:30:31 -070059
60
61def GenerateDotGraph(package, deps_map, options):
Alex Klein1699fab2022-09-08 08:46:06 -060062 """Generates the dot source for the dependency graph leading to a node.
Brian Harringf90d82a2012-08-23 08:30:31 -070063
Alex Klein1699fab2022-09-08 08:46:06 -060064 The output is a list of lines.
65 """
66 deps = GetReverseDependencyClosure(package, deps_map)
67 node = deps_map[package]
Brian Harringf90d82a2012-08-23 08:30:31 -070068
Alex Klein1699fab2022-09-08 08:46:06 -060069 # Keep track of all the emitted nodes so that we don't issue multiple
70 # definitions
71 emitted = set()
Brian Harringf90d82a2012-08-23 08:30:31 -070072
Alex Klein1699fab2022-09-08 08:46:06 -060073 graph = dot_helper.Graph(package)
Brian Harringf90d82a2012-08-23 08:30:31 -070074
Alex Klein1699fab2022-09-08 08:46:06 -060075 # Add all the children if we want them, all of them in their own subgraph,
76 # as a sink. Keep the arcs outside of the subgraph though (it generates
77 # better layout).
78 children_subgraph = None
79 if options.children and node["deps"]:
80 children_subgraph = graph.AddNewSubgraph("sink")
81 for child in node["deps"]:
82 child_node = deps_map[child]
83 AddNodeToSubgraph(
84 children_subgraph, child_node, options, CHILD_COLOR
85 )
86 emitted.add(child)
87 graph.AddArc(package, child)
Brian Harringf90d82a2012-08-23 08:30:31 -070088
Alex Klein1699fab2022-09-08 08:46:06 -060089 # Add the package in its own subgraph. If we didn't have children, make it
90 # a sink
91 if children_subgraph:
92 rank = "same"
93 else:
94 rank = "sink"
95 package_subgraph = graph.AddNewSubgraph(rank)
96 AddNodeToSubgraph(package_subgraph, node, options, TARGET_COLOR)
97 emitted.add(package)
Brian Harringf90d82a2012-08-23 08:30:31 -070098
Alex Klein1699fab2022-09-08 08:46:06 -060099 # Add all the other nodes, as well as all the arcs.
100 for dep in deps:
101 dep_node = deps_map[dep]
102 if not dep in emitted:
103 color = NORMAL_COLOR
104 if dep_node["action"] == "seed":
105 color = SEED_COLOR
106 AddNodeToSubgraph(graph, dep_node, options, color)
107 for j in dep_node["rev_deps"]:
108 graph.AddArc(j, dep)
Brian Harringf90d82a2012-08-23 08:30:31 -0700109
Alex Klein1699fab2022-09-08 08:46:06 -0600110 return graph.Gen()
Brian Harringf90d82a2012-08-23 08:30:31 -0700111
112
113def GenerateImages(data, options):
Alex Klein1699fab2022-09-08 08:46:06 -0600114 """Generate the output images for all the nodes in the input."""
115 deps_map = json.loads(data)
Brian Harringf90d82a2012-08-23 08:30:31 -0700116
Alex Klein1699fab2022-09-08 08:46:06 -0600117 for package in deps_map:
118 lines = GenerateDotGraph(package, deps_map, options)
Brian Harringf90d82a2012-08-23 08:30:31 -0700119
Alex Klein1699fab2022-09-08 08:46:06 -0600120 filename = os.path.join(
121 options.output_dir, GetOutputBaseName(deps_map[package], options)
122 )
Brian Harringf90d82a2012-08-23 08:30:31 -0700123
Alex Klein1699fab2022-09-08 08:46:06 -0600124 save_dot_filename = None
125 if options.save_dot:
126 save_dot_filename = filename + ".dot"
Brian Harringf90d82a2012-08-23 08:30:31 -0700127
Alex Klein1699fab2022-09-08 08:46:06 -0600128 dot_helper.GenerateImage(
129 lines, filename, options.format, save_dot_filename
130 )
Brian Harringf90d82a2012-08-23 08:30:31 -0700131
132
Mike Frysingerd6d39652016-09-01 00:27:59 -0400133def GetParser():
Alex Klein1699fab2022-09-08 08:46:06 -0600134 """Return a command line parser."""
135 parser = commandline.ArgumentParser(description=__doc__)
136 parser.add_argument(
137 "-f",
138 "--format",
139 default="svg",
140 help="Dot output format (png, svg, etc.).",
141 )
142 parser.add_argument(
143 "-o", "--output-dir", default=".", help="Output directory."
144 )
145 parser.add_argument(
146 "-c", "--children", action="store_true", help="Also add children."
147 )
148 parser.add_argument(
149 "-l", "--link", action="store_true", help="Embed links."
150 )
151 parser.add_argument(
152 "-b", "--base-url", default="", help="Base url for links."
153 )
154 parser.add_argument(
155 "-s", "--save-dot", action="store_true", help="Save dot files."
156 )
157 parser.add_argument("inputs", nargs="*", help="Chromium OS package lists")
158 return parser
Mike Frysingerd6d39652016-09-01 00:27:59 -0400159
160
Brian Harringf90d82a2012-08-23 08:30:31 -0700161def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600162 parser = GetParser()
163 options = parser.parse_args(argv)
164 options.Freeze()
Brian Harringf90d82a2012-08-23 08:30:31 -0700165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 try:
167 os.makedirs(options.output_dir)
168 except OSError:
169 # The directory already exists.
170 pass
Brian Harringf90d82a2012-08-23 08:30:31 -0700171
Alex Klein1699fab2022-09-08 08:46:06 -0600172 if not options.inputs:
173 GenerateImages(sys.stdin.read(), options)
174 else:
175 for i in options.inputs:
Mike Frysinger31fdddd2023-02-24 15:50:55 -0500176 with open(i, encoding="utf-8") as handle:
Alex Klein1699fab2022-09-08 08:46:06 -0600177 GenerateImages(handle.read(), options)