Joe Onorato | e5ed347 | 2024-02-02 14:52:05 -0800 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import argparse |
| 4 | import fnmatch |
| 5 | import json |
| 6 | import os |
| 7 | import pathlib |
| 8 | import types |
| 9 | import sys |
| 10 | |
| 11 | |
| 12 | class 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 | |
| 57 | class 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 | |
| 65 | PROVIDERS = [ |
| 66 | "android/soong/java.JarJarProviderData", |
| 67 | "android/soong/java.BaseJarJarProviderData", |
| 68 | ] |
| 69 | |
| 70 | |
| 71 | def 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 | |
| 90 | def format_source_pos(file, lineno): |
| 91 | result = file |
| 92 | if lineno: |
| 93 | result += f":{lineno}" |
| 94 | return result |
| 95 | |
| 96 | |
| 97 | STRIP_TYPE_PREFIXES = [ |
| 98 | "android/soong/", |
| 99 | "github.com/google/", |
| 100 | ] |
| 101 | |
| 102 | |
| 103 | def 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 | |
| 116 | def 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 | |
| 143 | def 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 | |
| 156 | def load_graph(): |
| 157 | modules=load_modules() |
| 158 | return Graph(modules) |
| 159 | |
| 160 | |
| 161 | def 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 | |
| 170 | def 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 | |
| 193 | def 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 | |
| 203 | def 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 | |
| 211 | class 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 | |
| 223 | class 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 | |
| 246 | class 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 | |
| 257 | class 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 | |
| 274 | COMMANDS = { |
| 275 | "between": BetweenCommand(), |
| 276 | "deps": DepsCommand(), |
| 277 | "id": IdCommand(), |
| 278 | "query": QueryCommand(), |
| 279 | } |
| 280 | |
| 281 | |
| 282 | def 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 | |
| 288 | ANDROID_BUILD_TOP = assert_env("ANDROID_BUILD_TOP") |
| 289 | |
| 290 | TARGET_PRODUCT = assert_env("TARGET_PRODUCT") |
| 291 | OUT_DIR = os.getenv("OUT_DIR") |
| 292 | if not OUT_DIR: |
| 293 | OUT_DIR = "out" |
| 294 | if OUT_DIR[0] != "/": |
| 295 | OUT_DIR = pathlib.Path(ANDROID_BUILD_TOP).joinpath(OUT_DIR) |
| 296 | SOONG_DEBUG_DATA_FILENAME = pathlib.Path(OUT_DIR).joinpath("soong/soong-debug-info.json") |
| 297 | |
| 298 | |
| 299 | def 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 | |
| 311 | if __name__ == "__main__": |
| 312 | main() |
| 313 | |