| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
|  | 2 |  | 
|  | 3 | import argparse | 
|  | 4 | import fnmatch | 
| Joe Onorato | 373dc18 | 2024-02-09 10:43:28 -0800 | [diff] [blame] | 5 | import html | 
| Joe Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 6 | import io | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 7 | import json | 
|  | 8 | import os | 
|  | 9 | import pathlib | 
| Joe Onorato | 373dc18 | 2024-02-09 10:43:28 -0800 | [diff] [blame] | 10 | import subprocess | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 11 | import types | 
|  | 12 | import sys | 
|  | 13 |  | 
|  | 14 |  | 
|  | 15 | class 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 |  | 
|  | 60 | class 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 |  | 
|  | 68 | PROVIDERS = [ | 
|  | 69 | "android/soong/java.JarJarProviderData", | 
|  | 70 | "android/soong/java.BaseJarJarProviderData", | 
|  | 71 | ] | 
|  | 72 |  | 
|  | 73 |  | 
| Joe Onorato | 373dc18 | 2024-02-09 10:43:28 -0800 | [diff] [blame] | 74 | def format_node_label(node, module_formatter): | 
|  | 75 | result = "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">" | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 76 |  | 
| Joe Onorato | 373dc18 | 2024-02-09 10:43:28 -0800 | [diff] [blame] | 77 | # 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 Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 89 | result += "</table>>" | 
|  | 90 | return result | 
|  | 91 |  | 
|  | 92 |  | 
|  | 93 | def format_source_pos(file, lineno): | 
|  | 94 | result = file | 
|  | 95 | if lineno: | 
|  | 96 | result += f":{lineno}" | 
|  | 97 | return result | 
|  | 98 |  | 
|  | 99 |  | 
|  | 100 | STRIP_TYPE_PREFIXES = [ | 
|  | 101 | "android/soong/", | 
|  | 102 | "github.com/google/", | 
|  | 103 | ] | 
|  | 104 |  | 
|  | 105 |  | 
|  | 106 | def 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 |  | 
|  | 119 | def 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 |  | 
|  | 146 | def 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 |  | 
|  | 159 | def load_graph(): | 
|  | 160 | modules=load_modules() | 
|  | 161 | return Graph(modules) | 
|  | 162 |  | 
|  | 163 |  | 
|  | 164 | def 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 |  | 
|  | 173 | def 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 Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 196 | def 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 |  | 
|  | 210 | def print_nodes(args, nodes, module_formatter): | 
|  | 211 | # Generate the graphviz | 
|  | 212 | dot = io.StringIO() | 
|  | 213 | dot.write("digraph {\n") | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 214 | for node in nodes: | 
| Joe Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 215 | dot.write(f"\"{node.id}\"[label={format_node_label(node, module_formatter)}];\n") | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 216 | for dep in node.deps: | 
|  | 217 | if dep in nodes: | 
| Joe Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 218 | 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 Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 231 |  | 
|  | 232 |  | 
|  | 233 | def 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 Onorato | 373dc18 | 2024-02-09 10:43:28 -0800 | [diff] [blame] | 241 | def 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 Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 265 | class BetweenCommand: | 
|  | 266 | help = "Print the module graph between two nodes." | 
|  | 267 |  | 
|  | 268 | def args(self, parser): | 
|  | 269 | parser.add_argument("module", nargs=2, | 
| Joe Onorato | 373dc18 | 2024-02-09 10:43:28 -0800 | [diff] [blame] | 270 | help="the two modules") | 
| Joe Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 271 | print_args(parser) | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 272 |  | 
|  | 273 | def run(self, args): | 
|  | 274 | graph = load_graph() | 
| Joe Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 275 | print_nodes(args, graph.find_paths(args.module[0], args.module[1]), | 
|  | 276 | new_module_formatter(args)) | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 277 |  | 
|  | 278 |  | 
|  | 279 | class 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 Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 285 | print_args(parser) | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 286 |  | 
|  | 287 | def run(self, args): | 
|  | 288 | graph = load_graph() | 
|  | 289 | nodes = set() | 
|  | 290 | err = False | 
| Joe Onorato | b3ffad1 | 2024-02-09 14:39:45 -0800 | [diff] [blame^] | 291 | for id in args.module: | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 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 Onorato | be952da | 2024-02-09 11:43:57 -0800 | [diff] [blame] | 300 | print_nodes(args, nodes, new_module_formatter(args)) | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 301 |  | 
|  | 302 |  | 
|  | 303 | class 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 |  | 
| Joe Onorato | 12e2cf7 | 2024-02-09 13:50:35 -0800 | [diff] [blame] | 314 | class JsonCommand: | 
|  | 315 | help = "Print metadata about modules in json format" | 
|  | 316 |  | 
|  | 317 | def args(self, parser): | 
|  | 318 | module_selection_args(parser) | 
|  | 319 | parser.add_argument("--list", action="store_true", | 
|  | 320 | help="Print the results in a json list. If not set and multiple" | 
|  | 321 | + " modules are matched, the output won't be valid json.") | 
|  | 322 |  | 
|  | 323 | def run(self, args): | 
|  | 324 | modules = load_and_filter_modules(args) | 
|  | 325 | if args.list: | 
|  | 326 | json.dump([m for m in modules], sys.stdout, indent=4, default=lambda o: o.__dict__) | 
|  | 327 | else: | 
|  | 328 | for m in modules: | 
|  | 329 | json.dump(m, sys.stdout, indent=4, default=lambda o: o.__dict__) | 
|  | 330 | print() | 
|  | 331 |  | 
|  | 332 |  | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 333 | class QueryCommand: | 
|  | 334 | help = "Query details about modules" | 
|  | 335 |  | 
|  | 336 | def args(self, parser): | 
|  | 337 | module_selection_args(parser) | 
|  | 338 |  | 
|  | 339 | def run(self, args): | 
|  | 340 | for m in load_and_filter_modules(args): | 
|  | 341 | print(m.id) | 
|  | 342 | print(f"    type:     {m.type}") | 
|  | 343 | print(f"    location: {format_source_pos(m.source_file, m.source_line)}") | 
|  | 344 | for p in m.providers: | 
|  | 345 | print(f"    provider: {format_provider(p)}") | 
|  | 346 | for d in m.deps: | 
|  | 347 | print(f"    dep:      {d.id}") | 
|  | 348 |  | 
|  | 349 |  | 
|  | 350 | COMMANDS = { | 
|  | 351 | "between": BetweenCommand(), | 
|  | 352 | "deps": DepsCommand(), | 
|  | 353 | "id": IdCommand(), | 
| Joe Onorato | 12e2cf7 | 2024-02-09 13:50:35 -0800 | [diff] [blame] | 354 | "json": JsonCommand(), | 
| Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame] | 355 | "query": QueryCommand(), | 
|  | 356 | } | 
|  | 357 |  | 
|  | 358 |  | 
|  | 359 | def assert_env(name): | 
|  | 360 | val = os.getenv(name) | 
|  | 361 | if not val: | 
|  | 362 | sys.stderr.write(f"{name} not set. please make sure you've run lunch.") | 
|  | 363 | return val | 
|  | 364 |  | 
|  | 365 | ANDROID_BUILD_TOP = assert_env("ANDROID_BUILD_TOP") | 
|  | 366 |  | 
|  | 367 | TARGET_PRODUCT = assert_env("TARGET_PRODUCT") | 
|  | 368 | OUT_DIR = os.getenv("OUT_DIR") | 
|  | 369 | if not OUT_DIR: | 
|  | 370 | OUT_DIR = "out" | 
|  | 371 | if OUT_DIR[0] != "/": | 
|  | 372 | OUT_DIR = pathlib.Path(ANDROID_BUILD_TOP).joinpath(OUT_DIR) | 
|  | 373 | SOONG_DEBUG_DATA_FILENAME = pathlib.Path(OUT_DIR).joinpath("soong/soong-debug-info.json") | 
|  | 374 |  | 
|  | 375 |  | 
|  | 376 | def main(): | 
|  | 377 | parser = argparse.ArgumentParser() | 
|  | 378 | subparsers = parser.add_subparsers(required=True, dest="command") | 
|  | 379 | for name in sorted(COMMANDS.keys()): | 
|  | 380 | command = COMMANDS[name] | 
|  | 381 | subparser = subparsers.add_parser(name, help=command.help) | 
|  | 382 | command.args(subparser) | 
|  | 383 | args = parser.parse_args() | 
|  | 384 | COMMANDS[args.command].run(args) | 
|  | 385 | sys.exit(0) | 
|  | 386 |  | 
|  | 387 |  | 
|  | 388 | if __name__ == "__main__": | 
|  | 389 | main() | 
|  | 390 |  |