| #!/usr/bin/env python3 |
| |
| import argparse |
| import fnmatch |
| import html |
| import io |
| import json |
| import os |
| import pathlib |
| import subprocess |
| import types |
| import sys |
| |
| |
| class Graph: |
| def __init__(self, modules): |
| def get_or_make_node(dictionary, id, module): |
| node = dictionary.get(id) |
| if node: |
| if module and not node.module: |
| node.module = module |
| return node |
| node = Node(id, module) |
| dictionary[id] = node |
| return node |
| self.nodes = dict() |
| for module in modules.values(): |
| node = get_or_make_node(self.nodes, module.id, module) |
| for d in module.deps: |
| dep = get_or_make_node(self.nodes, d.id, None) |
| node.deps.add(dep) |
| dep.rdeps.add(node) |
| node.dep_tags.setdefault(dep, list()).append(d) |
| |
| def find_paths(self, id1, id2): |
| # Throws KeyError if one of the names isn't found |
| def recurse(node1, node2, visited): |
| result = set() |
| for dep in node1.rdeps: |
| if dep == node2: |
| result.add(node2) |
| if dep not in visited: |
| visited.add(dep) |
| found = recurse(dep, node2, visited) |
| if found: |
| result |= found |
| result.add(dep) |
| return result |
| node1 = self.nodes[id1] |
| node2 = self.nodes[id2] |
| # Take either direction |
| p = recurse(node1, node2, set()) |
| if p: |
| p.add(node1) |
| return p |
| p = recurse(node2, node1, set()) |
| p.add(node2) |
| return p |
| |
| |
| class Node: |
| def __init__(self, id, module): |
| self.id = id |
| self.module = module |
| self.deps = set() |
| self.rdeps = set() |
| self.dep_tags = {} |
| |
| |
| PROVIDERS = [ |
| "android/soong/java.JarJarProviderData", |
| "android/soong/java.BaseJarJarProviderData", |
| ] |
| |
| |
| def format_dep_label(node, dep): |
| tags = node.dep_tags.get(dep) |
| labels = [] |
| if tags: |
| labels = [tag.tag_type.split("/")[-1] for tag in tags] |
| labels = sorted(set(labels)) |
| if labels: |
| result = "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">" |
| for label in labels: |
| result += f"<tr><td>{label}</td></tr>" |
| result += "</table>>" |
| return result |
| |
| |
| def format_node_label(node, module_formatter): |
| result = "<<table border=\"0\" cellborder=\"0\" cellspacing=\"0\" cellpadding=\"0\">" |
| |
| # node name |
| result += f"<tr><td><b>{node.module.name if node.module else node.id}</b></td></tr>" |
| |
| if node.module: |
| # node_type |
| result += f"<tr><td>{node.module.type}</td></tr>" |
| |
| # module_formatter will return a list of rows |
| for row in module_formatter(node.module): |
| row = html.escape(row) |
| result += f"<tr><td><font color=\"#666666\">{row}</font></td></tr>" |
| |
| result += "</table>>" |
| return result |
| |
| |
| def format_source_pos(file, lineno): |
| result = file |
| if lineno: |
| result += f":{lineno}" |
| return result |
| |
| |
| STRIP_TYPE_PREFIXES = [ |
| "android/soong/", |
| "github.com/google/", |
| ] |
| |
| |
| def format_provider(provider): |
| result = "" |
| for prefix in STRIP_TYPE_PREFIXES: |
| if provider.type.startswith(prefix): |
| result = provider.type[len(prefix):] |
| break |
| if not result: |
| result = provider.type |
| if True and provider.debug: |
| result += " (" + provider.debug + ")" |
| return result |
| |
| |
| def load_soong_debug(): |
| # Read the json |
| try: |
| with open(SOONG_DEBUG_DATA_FILENAME) as f: |
| info = json.load(f, object_hook=lambda d: types.SimpleNamespace(**d)) |
| except IOError: |
| sys.stderr.write(f"error: Unable to open {SOONG_DEBUG_DATA_FILENAME}. Make sure you have" |
| + " built with GENERATE_SOONG_DEBUG.\n") |
| sys.exit(1) |
| |
| # Construct IDs, which are name + variant if the |
| name_counts = dict() |
| for m in info.modules: |
| name_counts[m.name] = name_counts.get(m.name, 0) + 1 |
| def get_id(m): |
| result = m.name |
| if name_counts[m.name] > 1 and m.variant: |
| result += "@@" + m.variant |
| return result |
| for m in info.modules: |
| m.id = get_id(m) |
| for dep in m.deps: |
| dep.id = get_id(dep) |
| |
| return info |
| |
| |
| def load_modules(): |
| info = load_soong_debug() |
| |
| # Filter out unnamed modules |
| modules = dict() |
| for m in info.modules: |
| if not m.name: |
| continue |
| modules[m.id] = m |
| |
| return modules |
| |
| |
| def load_graph(): |
| modules=load_modules() |
| return Graph(modules) |
| |
| |
| def module_selection_args(parser): |
| parser.add_argument("modules", nargs="*", |
| help="Modules to match. Can be glob-style wildcards.") |
| parser.add_argument("--provider", nargs="+", |
| help="Match the given providers.") |
| parser.add_argument("--dep", nargs="+", |
| help="Match the given providers.") |
| |
| |
| def load_and_filter_modules(args): |
| # Which modules are printed |
| matchers = [] |
| if args.modules: |
| matchers.append(lambda m: [True for pattern in args.modules |
| if fnmatch.fnmatchcase(m.name, pattern)]) |
| if args.provider: |
| matchers.append(lambda m: [True for pattern in args.provider |
| if [True for p in m.providers if p.type.endswith(pattern)]]) |
| if args.dep: |
| matchers.append(lambda m: [True for pattern in args.dep |
| if [True for d in m.deps if d.id == pattern]]) |
| |
| if not matchers: |
| sys.stderr.write("error: At least one module matcher must be supplied\n") |
| sys.exit(1) |
| |
| info = load_soong_debug() |
| for m in sorted(info.modules, key=lambda m: (m.name, m.variant)): |
| if len([matcher for matcher in matchers if matcher(m)]) == len(matchers): |
| yield m |
| |
| |
| def print_args(parser): |
| parser.add_argument("--label", action="append", metavar="JQ_FILTER", |
| help="jq query for each module metadata") |
| parser.add_argument("--deptags", action="store_true", |
| help="show dependency tags (makes the graph much more complex)") |
| |
| group = parser.add_argument_group("output formats", |
| "If no format is provided, a dot file will be written to" |
| + " stdout.") |
| output = group.add_mutually_exclusive_group() |
| output.add_argument("--dot", type=str, metavar="FILENAME", |
| help="Write the graph to this file as dot (graphviz format)") |
| output.add_argument("--svg", type=str, metavar="FILENAME", |
| help="Write the graph to this file as svg") |
| |
| |
| def print_nodes(args, nodes, module_formatter): |
| # Generate the graphviz |
| dep_tag_id = 0 |
| dot = io.StringIO() |
| dot.write("digraph {\n") |
| dot.write("node [shape=box];") |
| |
| for node in nodes: |
| dot.write(f"\"{node.id}\" [label={format_node_label(node, module_formatter)}];\n") |
| for dep in node.deps: |
| if dep in nodes: |
| if args.deptags: |
| dot.write(f"\"{node.id}\" -> \"__dep_tag_{dep_tag_id}\" [ arrowhead=none ];\n") |
| dot.write(f"\"__dep_tag_{dep_tag_id}\" -> \"{dep.id}\";\n") |
| dot.write(f"\"__dep_tag_{dep_tag_id}\"" |
| + f"[label={format_dep_label(node, dep)} shape=ellipse" |
| + " color=\"#666666\" fontcolor=\"#666666\"];\n") |
| else: |
| dot.write(f"\"{node.id}\" -> \"{dep.id}\";\n") |
| dep_tag_id += 1 |
| dot.write("}\n") |
| text = dot.getvalue() |
| |
| # Write it somewhere |
| if args.dot: |
| with open(args.dot, "w") as f: |
| f.write(text) |
| elif args.svg: |
| subprocess.run(["dot", "-Tsvg", "-o", args.svg], |
| input=text, text=True, check=True) |
| else: |
| sys.stdout.write(text) |
| |
| |
| def get_deps(nodes, root, maxdepth, reverse): |
| if root in nodes: |
| return |
| nodes.add(root) |
| if maxdepth != 0: |
| for dep in (root.rdeps if reverse else root.deps): |
| get_deps(nodes, dep, maxdepth-1, reverse) |
| |
| |
| def new_module_formatter(args): |
| def module_formatter(module): |
| if not args.label: |
| return [] |
| result = [] |
| text = json.dumps(module, default=lambda o: o.__dict__) |
| for jq_filter in args.label: |
| proc = subprocess.run(["jq", jq_filter], |
| input=text, text=True, check=True, stdout=subprocess.PIPE) |
| if proc.stdout: |
| o = json.loads(proc.stdout) |
| if type(o) == list: |
| for row in o: |
| if row: |
| result.append(row) |
| elif type(o) == dict: |
| result.append(str(proc.stdout).strip()) |
| else: |
| if o: |
| result.append(str(o).strip()) |
| return result |
| return module_formatter |
| |
| |
| class BetweenCommand: |
| help = "Print the module graph between two nodes." |
| |
| def args(self, parser): |
| parser.add_argument("module", nargs=2, |
| help="the two modules") |
| print_args(parser) |
| |
| def run(self, args): |
| graph = load_graph() |
| print_nodes(args, graph.find_paths(args.module[0], args.module[1]), |
| new_module_formatter(args)) |
| |
| |
| class DepsCommand: |
| help = "Print the module graph of dependencies of one or more modules" |
| |
| def args(self, parser): |
| parser.add_argument("module", nargs="+", |
| help="Module to print dependencies of") |
| parser.add_argument("--reverse", action="store_true", |
| help="traverse reverse dependencies") |
| parser.add_argument("--depth", type=int, default=-1, |
| help="max depth of dependencies (can keep the graph size reasonable)") |
| print_args(parser) |
| |
| def run(self, args): |
| graph = load_graph() |
| nodes = set() |
| err = False |
| for id in args.module: |
| root = graph.nodes.get(id) |
| if not root: |
| sys.stderr.write(f"error: Can't find root: {id}\n") |
| err = True |
| continue |
| get_deps(nodes, root, args.depth, args.reverse) |
| if err: |
| sys.exit(1) |
| print_nodes(args, nodes, new_module_formatter(args)) |
| |
| |
| class IdCommand: |
| help = "Print the id (name + variant) of matching modules" |
| |
| def args(self, parser): |
| module_selection_args(parser) |
| |
| def run(self, args): |
| for m in load_and_filter_modules(args): |
| print(m.id) |
| |
| |
| class JsonCommand: |
| help = "Print metadata about modules in json format" |
| |
| def args(self, parser): |
| module_selection_args(parser) |
| parser.add_argument("--list", action="store_true", |
| help="Print the results in a json list. If not set and multiple" |
| + " modules are matched, the output won't be valid json.") |
| |
| def run(self, args): |
| modules = load_and_filter_modules(args) |
| if args.list: |
| json.dump([m for m in modules], sys.stdout, indent=4, default=lambda o: o.__dict__) |
| else: |
| for m in modules: |
| json.dump(m, sys.stdout, indent=4, default=lambda o: o.__dict__) |
| print() |
| |
| |
| class QueryCommand: |
| help = "Query details about modules" |
| |
| def args(self, parser): |
| module_selection_args(parser) |
| |
| def run(self, args): |
| for m in load_and_filter_modules(args): |
| print(m.id) |
| print(f" type: {m.type}") |
| print(f" location: {format_source_pos(m.source_file, m.source_line)}") |
| for p in m.providers: |
| print(f" provider: {format_provider(p)}") |
| for d in m.deps: |
| print(f" dep: {d.id}") |
| |
| |
| COMMANDS = { |
| "between": BetweenCommand(), |
| "deps": DepsCommand(), |
| "id": IdCommand(), |
| "json": JsonCommand(), |
| "query": QueryCommand(), |
| } |
| |
| |
| def assert_env(name): |
| val = os.getenv(name) |
| if not val: |
| sys.stderr.write(f"{name} not set. please make sure you've run lunch.") |
| return val |
| |
| ANDROID_BUILD_TOP = assert_env("ANDROID_BUILD_TOP") |
| |
| TARGET_PRODUCT = assert_env("TARGET_PRODUCT") |
| OUT_DIR = os.getenv("OUT_DIR") |
| if not OUT_DIR: |
| OUT_DIR = "out" |
| if OUT_DIR[0] != "/": |
| OUT_DIR = pathlib.Path(ANDROID_BUILD_TOP).joinpath(OUT_DIR) |
| SOONG_DEBUG_DATA_FILENAME = pathlib.Path(OUT_DIR).joinpath("soong/soong-debug-info.json") |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| subparsers = parser.add_subparsers(required=True, dest="command") |
| for name in sorted(COMMANDS.keys()): |
| command = COMMANDS[name] |
| subparser = subparsers.add_parser(name, help=command.help) |
| command.args(subparser) |
| args = parser.parse_args() |
| COMMANDS[args.command].run(args) |
| sys.exit(0) |
| |
| |
| if __name__ == "__main__": |
| main() |
| |