2816c97d23
Test: use the flags Change-Id: If7edacae672eca974978cc211d6e385d5038bcc2
424 lines
13 KiB
Python
Executable file
424 lines
13 KiB
Python
Executable file
#!/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()
|
|
|