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