blob: c091c972bfbac1f6176c09cb6f5ebb652021537e [file] [log] [blame]
Joe Onoratoe5ed3472024-02-02 14:52:05 -08001#!/usr/bin/env python3
2
3import argparse
4import fnmatch
Joe Onorato373dc182024-02-09 10:43:28 -08005import html
Joe Onoratobe952da2024-02-09 11:43:57 -08006import io
Joe Onoratoe5ed3472024-02-02 14:52:05 -08007import json
8import os
9import pathlib
Joe Onorato373dc182024-02-09 10:43:28 -080010import subprocess
Joe Onoratoe5ed3472024-02-02 14:52:05 -080011import types
12import sys
13
14
15class Graph:
16 def __init__(self, modules):
17 def get_or_make_node(dictionary, id, module):
18 node = dictionary.get(id)
19 if node:
20 if module and not node.module:
21 node.module = module
22 return node
23 node = Node(id, module)
24 dictionary[id] = node
25 return node
26 self.nodes = dict()
27 for module in modules.values():
28 node = get_or_make_node(self.nodes, module.id, module)
29 for d in module.deps:
30 dep = get_or_make_node(self.nodes, d.id, None)
31 node.deps.add(dep)
32 dep.rdeps.add(node)
33
34 def find_paths(self, id1, id2):
35 # Throws KeyError if one of the names isn't found
36 def recurse(node1, node2, visited):
37 result = set()
38 for dep in node1.rdeps:
39 if dep == node2:
40 result.add(node2)
41 if dep not in visited:
42 visited.add(dep)
43 found = recurse(dep, node2, visited)
44 if found:
45 result |= found
46 result.add(dep)
47 return result
48 node1 = self.nodes[id1]
49 node2 = self.nodes[id2]
50 # Take either direction
51 p = recurse(node1, node2, set())
52 if p:
53 p.add(node1)
54 return p
55 p = recurse(node2, node1, set())
56 p.add(node2)
57 return p
58
59
60class Node:
61 def __init__(self, id, module):
62 self.id = id
63 self.module = module
64 self.deps = set()
65 self.rdeps = set()
66
67
68PROVIDERS = [
69 "android/soong/java.JarJarProviderData",
70 "android/soong/java.BaseJarJarProviderData",
71]
72
73
Joe Onorato373dc182024-02-09 10:43:28 -080074def format_node_label(node, module_formatter):
75 result = "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">"
Joe Onoratoe5ed3472024-02-02 14:52:05 -080076
Joe Onorato373dc182024-02-09 10:43:28 -080077 # node name
78 result += f"<tr><td><b>{node.module.name if node.module else node.id}</b></td></tr>"
79
80 if node.module:
81 # node_type
82 result += f"<tr><td>{node.module.type}</td></tr>"
83
84 # module_formatter will return a list of rows
85 for row in module_formatter(node.module):
86 row = html.escape(row)
87 result += f"<tr><td><font color=\"#666666\">{row}</font></td></tr>"
88
Joe Onoratoe5ed3472024-02-02 14:52:05 -080089 result += "</table>>"
90 return result
91
92
93def format_source_pos(file, lineno):
94 result = file
95 if lineno:
96 result += f":{lineno}"
97 return result
98
99
100STRIP_TYPE_PREFIXES = [
101 "android/soong/",
102 "github.com/google/",
103]
104
105
106def format_provider(provider):
107 result = ""
108 for prefix in STRIP_TYPE_PREFIXES:
109 if provider.type.startswith(prefix):
110 result = provider.type[len(prefix):]
111 break
112 if not result:
113 result = provider.type
114 if True and provider.debug:
115 result += " (" + provider.debug + ")"
116 return result
117
118
119def load_soong_debug():
120 # Read the json
121 try:
122 with open(SOONG_DEBUG_DATA_FILENAME) as f:
123 info = json.load(f, object_hook=lambda d: types.SimpleNamespace(**d))
124 except IOError:
125 sys.stderr.write(f"error: Unable to open {SOONG_DEBUG_DATA_FILENAME}. Make sure you have"
126 + " built with GENERATE_SOONG_DEBUG.\n")
127 sys.exit(1)
128
129 # Construct IDs, which are name + variant if the
130 name_counts = dict()
131 for m in info.modules:
132 name_counts[m.name] = name_counts.get(m.name, 0) + 1
133 def get_id(m):
134 result = m.name
135 if name_counts[m.name] > 1 and m.variant:
136 result += "@@" + m.variant
137 return result
138 for m in info.modules:
139 m.id = get_id(m)
140 for dep in m.deps:
141 dep.id = get_id(dep)
142
143 return info
144
145
146def load_modules():
147 info = load_soong_debug()
148
149 # Filter out unnamed modules
150 modules = dict()
151 for m in info.modules:
152 if not m.name:
153 continue
154 modules[m.id] = m
155
156 return modules
157
158
159def load_graph():
160 modules=load_modules()
161 return Graph(modules)
162
163
164def module_selection_args(parser):
165 parser.add_argument("modules", nargs="*",
166 help="Modules to match. Can be glob-style wildcards.")
167 parser.add_argument("--provider", nargs="+",
168 help="Match the given providers.")
169 parser.add_argument("--dep", nargs="+",
170 help="Match the given providers.")
171
172
173def load_and_filter_modules(args):
174 # Which modules are printed
175 matchers = []
176 if args.modules:
177 matchers.append(lambda m: [True for pattern in args.modules
178 if fnmatch.fnmatchcase(m.name, pattern)])
179 if args.provider:
180 matchers.append(lambda m: [True for pattern in args.provider
181 if [True for p in m.providers if p.type.endswith(pattern)]])
182 if args.dep:
183 matchers.append(lambda m: [True for pattern in args.dep
184 if [True for d in m.deps if d.id == pattern]])
185
186 if not matchers:
187 sys.stderr.write("error: At least one module matcher must be supplied\n")
188 sys.exit(1)
189
190 info = load_soong_debug()
191 for m in sorted(info.modules, key=lambda m: (m.name, m.variant)):
192 if len([matcher for matcher in matchers if matcher(m)]) == len(matchers):
193 yield m
194
195
Joe Onoratobe952da2024-02-09 11:43:57 -0800196def print_args(parser):
197 parser.add_argument("--label", action="append", metavar="JQ_FILTER",
198 help="jq query for each module metadata")
199
200 group = parser.add_argument_group("output formats",
201 "If no format is provided, a dot file will be written to"
202 + " stdout.")
203 output = group.add_mutually_exclusive_group()
204 output.add_argument("--dot", type=str, metavar="FILENAME",
205 help="Write the graph to this file as dot (graphviz format)")
206 output.add_argument("--svg", type=str, metavar="FILENAME",
207 help="Write the graph to this file as svg")
208
209
210def print_nodes(args, nodes, module_formatter):
211 # Generate the graphviz
212 dot = io.StringIO()
213 dot.write("digraph {\n")
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800214 for node in nodes:
Joe Onoratobe952da2024-02-09 11:43:57 -0800215 dot.write(f"\"{node.id}\"[label={format_node_label(node, module_formatter)}];\n")
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800216 for dep in node.deps:
217 if dep in nodes:
Joe Onoratobe952da2024-02-09 11:43:57 -0800218 dot.write(f"\"{node.id}\" -> \"{dep.id}\";\n")
219 dot.write("}\n")
220 text = dot.getvalue()
221
222 # Write it somewhere
223 if args.dot:
224 with open(args.dot, "w") as f:
225 f.write(text)
226 elif args.svg:
227 subprocess.run(["dot", "-Tsvg", "-Nshape=box", "-o", args.svg],
228 input=text, text=True, check=True)
229 else:
230 sys.stdout.write(text)
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800231
232
233def get_deps(nodes, root):
234 if root in nodes:
235 return
236 nodes.add(root)
237 for dep in root.deps:
238 get_deps(nodes, dep)
239
240
Joe Onorato373dc182024-02-09 10:43:28 -0800241def new_module_formatter(args):
242 def module_formatter(module):
243 if not args.label:
244 return []
245 result = []
246 text = json.dumps(module, default=lambda o: o.__dict__)
247 for jq_filter in args.label:
248 proc = subprocess.run(["jq", jq_filter],
249 input=text, text=True, check=True, stdout=subprocess.PIPE)
250 if proc.stdout:
251 o = json.loads(proc.stdout)
252 if type(o) == list:
253 for row in o:
254 if row:
255 result.append(row)
256 elif type(o) == dict:
257 result.append(str(proc.stdout).strip())
258 else:
259 if o:
260 result.append(str(o).strip())
261 return result
262 return module_formatter
263
264
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800265class BetweenCommand:
266 help = "Print the module graph between two nodes."
267
268 def args(self, parser):
269 parser.add_argument("module", nargs=2,
Joe Onorato373dc182024-02-09 10:43:28 -0800270 help="the two modules")
Joe Onoratobe952da2024-02-09 11:43:57 -0800271 print_args(parser)
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800272
273 def run(self, args):
274 graph = load_graph()
Joe Onoratobe952da2024-02-09 11:43:57 -0800275 print_nodes(args, graph.find_paths(args.module[0], args.module[1]),
276 new_module_formatter(args))
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800277
278
279class DepsCommand:
280 help = "Print the module graph of dependencies of one or more modules"
281
282 def args(self, parser):
283 parser.add_argument("module", nargs="+",
284 help="Module to print dependencies of")
Joe Onoratobe952da2024-02-09 11:43:57 -0800285 print_args(parser)
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800286
287 def run(self, args):
288 graph = load_graph()
289 nodes = set()
290 err = False
291 for id in sys.argv[3:]:
292 root = graph.nodes.get(id)
293 if not root:
294 sys.stderr.write(f"error: Can't find root: {id}\n")
295 err = True
296 continue
297 get_deps(nodes, root)
298 if err:
299 sys.exit(1)
Joe Onoratobe952da2024-02-09 11:43:57 -0800300 print_nodes(args, nodes, new_module_formatter(args))
Joe Onoratoe5ed3472024-02-02 14:52:05 -0800301
302
303class IdCommand:
304 help = "Print the id (name + variant) of matching modules"
305
306 def args(self, parser):
307 module_selection_args(parser)
308
309 def run(self, args):
310 for m in load_and_filter_modules(args):
311 print(m.id)
312
313
314class QueryCommand:
315 help = "Query details about modules"
316
317 def args(self, parser):
318 module_selection_args(parser)
319
320 def run(self, args):
321 for m in load_and_filter_modules(args):
322 print(m.id)
323 print(f" type: {m.type}")
324 print(f" location: {format_source_pos(m.source_file, m.source_line)}")
325 for p in m.providers:
326 print(f" provider: {format_provider(p)}")
327 for d in m.deps:
328 print(f" dep: {d.id}")
329
330
331COMMANDS = {
332 "between": BetweenCommand(),
333 "deps": DepsCommand(),
334 "id": IdCommand(),
335 "query": QueryCommand(),
336}
337
338
339def assert_env(name):
340 val = os.getenv(name)
341 if not val:
342 sys.stderr.write(f"{name} not set. please make sure you've run lunch.")
343 return val
344
345ANDROID_BUILD_TOP = assert_env("ANDROID_BUILD_TOP")
346
347TARGET_PRODUCT = assert_env("TARGET_PRODUCT")
348OUT_DIR = os.getenv("OUT_DIR")
349if not OUT_DIR:
350 OUT_DIR = "out"
351if OUT_DIR[0] != "/":
352 OUT_DIR = pathlib.Path(ANDROID_BUILD_TOP).joinpath(OUT_DIR)
353SOONG_DEBUG_DATA_FILENAME = pathlib.Path(OUT_DIR).joinpath("soong/soong-debug-info.json")
354
355
356def main():
357 parser = argparse.ArgumentParser()
358 subparsers = parser.add_subparsers(required=True, dest="command")
359 for name in sorted(COMMANDS.keys()):
360 command = COMMANDS[name]
361 subparser = subparsers.add_parser(name, help=command.help)
362 command.args(subparser)
363 args = parser.parse_args()
364 COMMANDS[args.command].run(args)
365 sys.exit(0)
366
367
368if __name__ == "__main__":
369 main()
370