| #!/usr/bin/env python3 |
| # Copyright 2020 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Command-line tool to run jdeps and process its output into a JSON file.""" |
| |
| import argparse |
| import functools |
| import math |
| import multiprocessing |
| import pathlib |
| import os |
| |
| from typing import List, Tuple |
| |
| import class_dependency |
| import git_utils |
| import package_dependency |
| import serialization |
| import subprocess_utils |
| |
| DEFAULT_ROOT_TARGET = 'chrome/android:monochrome_public_bundle' |
| |
| |
| def class_is_interesting(name: str): |
| """Checks if a jdeps class is a class we are actually interested in.""" |
| if name.startswith('org.chromium.'): |
| return True |
| return False |
| |
| |
| # pylint: disable=useless-object-inheritance |
| class JavaClassJdepsParser(object): |
| """A parser for jdeps class-level dependency output.""" |
| def __init__(self): # pylint: disable=missing-function-docstring |
| self._graph = class_dependency.JavaClassDependencyGraph() |
| |
| @property |
| def graph(self): |
| """The dependency graph of the jdeps output. |
| |
| Initialized as empty and updated using parse_raw_jdeps_output. |
| """ |
| return self._graph |
| |
| def parse_raw_jdeps_output(self, build_target: str, jdeps_output: str): |
| """Parses the entirety of the jdeps output.""" |
| for line in jdeps_output.split('\n'): |
| self.parse_line(build_target, line) |
| |
| def parse_line(self, build_target: str, line: str): |
| """Parses a line of jdeps output. |
| |
| The assumed format of the line starts with 'name_1 -> name_2'. |
| """ |
| parsed = line.split() |
| if len(parsed) <= 3: |
| return |
| if parsed[2] == 'not' and parsed[3] == 'found': |
| return |
| if parsed[1] != '->': |
| return |
| |
| dep_from = parsed[0] |
| dep_to = parsed[2] |
| if not class_is_interesting(dep_from): |
| return |
| |
| key_from, nested_from = class_dependency.split_nested_class_from_key( |
| dep_from) |
| from_node: class_dependency.JavaClass = self._graph.add_node_if_new( |
| key_from) |
| from_node.add_build_target(build_target) |
| |
| if not class_is_interesting(dep_to): |
| return |
| |
| key_to, nested_to = class_dependency.split_nested_class_from_key( |
| dep_to) |
| |
| self._graph.add_node_if_new(key_to) |
| if key_from != key_to: # Skip self-edges (class-nested dependency) |
| self._graph.add_edge_if_new(key_from, key_to) |
| if nested_from is not None: |
| from_node.add_nested_class(nested_from) |
| if nested_to is not None: |
| from_node.add_nested_class(nested_to) |
| |
| |
| def _run_jdeps(jdeps_path: str, filepath: pathlib.Path) -> str: |
| """Runs jdeps on the given filepath and returns the output.""" |
| print(f'Running jdeps and parsing output for {filepath}') |
| return subprocess_utils.run_command( |
| [jdeps_path, '-R', '-verbose:class', filepath]) |
| |
| |
| def _run_gn_desc_list_dependencies(build_output_dir: str, target: str, |
| gn_path: str) -> str: |
| """Runs gn desc to list all jars that a target depends on. |
| |
| This includes direct and indirect dependencies.""" |
| return subprocess_utils.run_command( |
| [gn_path, 'desc', '--all', build_output_dir, target, 'deps']) |
| |
| |
| JarTargetList = List[Tuple[str, pathlib.Path]] |
| |
| |
| def list_original_targets_and_jars(gn_desc_output: str, build_output_dir: str, |
| cr_position: int) -> JarTargetList: |
| """Parses gn desc output to list original java targets and output jar paths. |
| |
| Returns a list of tuples (build_target: str, jar_path: str), where: |
| - build_target is the original java dependency target in the form |
| "//path/to:target" |
| - jar_path is the path to the built jar in the build_output_dir, |
| including the path to the output dir |
| """ |
| jar_tuples: JarTargetList = [] |
| for build_target_line in gn_desc_output.split('\n'): |
| if not build_target_line.endswith('__compile_java'): |
| continue |
| build_target = build_target_line.strip() |
| original_build_target = build_target.replace('__compile_java', '') |
| jar_path = _get_jar_path_for_target(build_output_dir, build_target, |
| cr_position) |
| jar_tuples.append((original_build_target, jar_path)) |
| return jar_tuples |
| |
| |
| def _get_jar_path_for_target(build_output_dir: str, build_target: str, |
| cr_position: int) -> str: |
| if cr_position == 0: # Not running on main branch, use current convention. |
| subdirectory = 'obj' |
| elif cr_position < 761560: # crrev.com/c/2161205 |
| subdirectory = 'gen' |
| else: |
| subdirectory = 'obj' |
| """Calculates the output location of a jar for a java build target.""" |
| target_path, target_name = build_target.split(':') |
| assert target_path.startswith('//'), \ |
| f'Build target should start with "//" but is: "{build_target}"' |
| jar_dir = target_path[len('//'):] |
| jar_name = target_name.replace('__compile_java', '.javac.jar') |
| return pathlib.Path(build_output_dir) / subdirectory / jar_dir / jar_name |
| |
| |
| def main(): |
| """Runs jdeps on all JARs a build target depends on. |
| |
| Creates a JSON file from the jdeps output.""" |
| arg_parser = argparse.ArgumentParser( |
| description='Runs jdeps (dependency analysis tool) on all JARs a root ' |
| 'build target depends on and writes the resulting dependency graph ' |
| 'into a JSON file. The default root build target is ' |
| 'chrome/android:monochrome_public_bundle.') |
| required_arg_group = arg_parser.add_argument_group('required arguments') |
| required_arg_group.add_argument('-C', |
| '--build_output_dir', |
| required=True, |
| help='Build output directory.') |
| required_arg_group.add_argument( |
| '-o', |
| '--output', |
| required=True, |
| help='Path to the file to write JSON output to. Will be created ' |
| 'if it does not yet exist and overwrite existing ' |
| 'content if it does.') |
| arg_parser.add_argument('-t', |
| '--target', |
| default=DEFAULT_ROOT_TARGET, |
| help='Root build target.') |
| arg_parser.add_argument('-d', |
| '--checkout-dir', |
| help='Path to the chromium checkout directory.') |
| arg_parser.add_argument('-j', |
| '--jdeps-path', |
| help='Path to the jdeps executable.') |
| arg_parser.add_argument('-g', |
| '--gn-path', |
| default='gn', |
| help='Path to the gn executable.') |
| arguments = arg_parser.parse_args() |
| |
| if arguments.checkout_dir: |
| src_path = pathlib.Path(arguments.checkout_dir) |
| else: |
| src_path = pathlib.Path(__file__).resolve().parents[3] |
| |
| if arguments.jdeps_path: |
| jdeps_path = pathlib.Path(arguments.jdeps_path) |
| else: |
| jdeps_path = src_path.joinpath('third_party/jdk/current/bin/jdeps') |
| |
| # gn and git must be run from inside the git checkout. |
| os.chdir(src_path) |
| |
| cr_position_str = git_utils.get_last_commit_cr_position() |
| cr_position = int(cr_position_str) if cr_position_str else 0 |
| |
| print('Getting list of dependency jars...') |
| gn_desc_output = _run_gn_desc_list_dependencies(arguments.build_output_dir, |
| arguments.target, |
| arguments.gn_path) |
| target_jars: JarTargetList = list_original_targets_and_jars( |
| gn_desc_output, arguments.build_output_dir, cr_position) |
| |
| print('Running jdeps...') |
| # jdeps already has some parallelism |
| jdeps_process_number = math.ceil(multiprocessing.cpu_count() / 2) |
| with multiprocessing.Pool(jdeps_process_number) as pool: |
| jar_paths = [target_jar for _, target_jar in target_jars] |
| jdeps_outputs = pool.map(functools.partial(_run_jdeps, jdeps_path), |
| jar_paths) |
| |
| print('Parsing jdeps output...') |
| jdeps_parser = JavaClassJdepsParser() |
| for raw_jdeps_output, (build_target, _) in zip(jdeps_outputs, target_jars): |
| jdeps_parser.parse_raw_jdeps_output(build_target, raw_jdeps_output) |
| |
| class_graph = jdeps_parser.graph |
| print(f'Parsed class-level dependency graph, ' |
| f'got {class_graph.num_nodes} nodes ' |
| f'and {class_graph.num_edges} edges.') |
| |
| package_graph = package_dependency.JavaPackageDependencyGraph(class_graph) |
| print(f'Created package-level dependency graph, ' |
| f'got {package_graph.num_nodes} nodes ' |
| f'and {package_graph.num_edges} edges.') |
| |
| print(f'Dumping JSON representation to {arguments.output}.') |
| serialization.dump_class_and_package_graphs_to_file( |
| class_graph, package_graph, arguments.output) |
| |
| |
| if __name__ == '__main__': |
| main() |