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