#!/usr/bin/env python3 # Copyright 2017 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Wraps bin/helper/bytecode_processor and expands @FileArgs.""" import argparse import collections import logging import pathlib import shlex import sys from typing import Dict, List, Tuple from util import build_utils from util import dep_utils from util import jar_utils from util import server_utils import action_helpers # build_utils adds //build to sys.path. _SRC_PATH = pathlib.Path(build_utils.DIR_SOURCE_ROOT).resolve() sys.path.append(str(_SRC_PATH / 'build/gn_ast')) from gn_editor import NO_VALID_GN_STR def _ShouldIgnoreDep(dep_name: str): if 'gen.base_module.R' in dep_name: return True return False def _ParseDepGraph(jar_path: str): output = jar_utils.run_jdeps(pathlib.Path(jar_path)) assert output is not None, f'Unable to parse jdep for {jar_path}' dep_graph = collections.defaultdict(set) # pylint: disable=line-too-long # Example output: # java.javac.jar -> java.base # java.javac.jar -> not found # org.chromium.chrome.browser.tabmodel.AsyncTabParamsManagerFactory -> java.lang.Object java.base # org.chromium.chrome.browser.tabmodel.TabWindowManagerImpl -> org.chromium.base.ApplicationStatus not found # org.chromium.chrome.browser.tabmodel.TabWindowManagerImpl -> org.chromium.base.ApplicationStatus$ActivityStateListener not found # org.chromium.chrome.browser.tabmodel.TabWindowManagerImpl -> org.chromium.chrome.browser.tab.Tab not found # pylint: enable=line-too-long for line in output.splitlines(): parsed = line.split() # E.g. java.javac.jar -> java.base if len(parsed) <= 3: continue # E.g. java.javac.jar -> not found if parsed[2] == 'not' and parsed[3] == 'found': continue if parsed[1] != '->': continue dep_from = parsed[0] dep_to = parsed[2] dep_graph[dep_from].add(dep_to) return dep_graph def _EnsureDirectClasspathIsComplete( *, input_jar: str, gn_target: str, output_dir: str, sdk_classpath_jars: List[str], direct_classpath_jars: List[str], full_classpath_jars: List[str], full_classpath_gn_targets: List[str], warnings_as_errors: bool, auto_add_deps: bool, ): logging.info('Parsing %d direct classpath jars', len(sdk_classpath_jars)) sdk_classpath_deps = set() for jar in sdk_classpath_jars: deps = jar_utils.extract_full_class_names_from_jar(jar) sdk_classpath_deps.update(deps) logging.info('Parsing %d direct classpath jars', len(direct_classpath_jars)) direct_classpath_deps = set() for jar in direct_classpath_jars: deps = jar_utils.extract_full_class_names_from_jar(jar) direct_classpath_deps.update(deps) logging.info('Parsing %d full classpath jars', len(full_classpath_jars)) full_classpath_deps = set() dep_to_target = collections.defaultdict(set) for jar, target in zip(full_classpath_jars, full_classpath_gn_targets): deps = jar_utils.extract_full_class_names_from_jar(jar) full_classpath_deps.update(deps) for dep in deps: dep_to_target[dep].add(target) transitive_deps = full_classpath_deps - direct_classpath_deps missing_classes: Dict[str, str] = {} dep_graph = _ParseDepGraph(input_jar) logging.info('Finding missing deps from %d classes', len(dep_graph)) # dep_graph.keys() is a list of all the classes in the current input_jar. Skip # all of these to avoid checking dependencies in the same target (e.g. A # depends on B, but both A and B are in input_jar). # Since the bundle will always have access to classes in the current android # sdk, those should not be considered missing. seen_deps = set(dep_graph.keys()) | sdk_classpath_deps for dep_from, deps_to in dep_graph.items(): for dep_to in deps_to - seen_deps: if _ShouldIgnoreDep(dep_to): continue seen_deps.add(dep_to) if dep_to in transitive_deps: # Allow clobbering since it doesn't matter which specific class depends # on |dep_to|. missing_classes[dep_to] = dep_from # missing_target_names = tuple(sorted(dep_to_target[dep_to])) # missing_targets[missing_target_names][dep_to] = dep_from if missing_classes: class_lookup_index = dep_utils.ClassLookupIndex(pathlib.Path(output_dir), should_build=False) missing_deps = set() for dep_to in missing_classes: # Using dep_utils.ClassLookupIndex ensures we respect the preferred dep # if any exists for the missing deps. suggested_deps = class_lookup_index.match(dep_to) assert suggested_deps, f'Unable to find target for {dep_to}' suggested_deps = dep_utils.DisambiguateDeps(suggested_deps) missing_deps.add(suggested_deps[0].target) cmd = dep_utils.CreateAddDepsCommand(gn_target, sorted(missing_deps)) def print_and_maybe_exit(): missing_targets: Dict[Tuple, List[str]] = collections.defaultdict(list) for dep_to, dep_from in missing_classes.items(): missing_target_names = tuple(sorted(dep_to_target[dep_to])) missing_targets[missing_target_names].append(dep_to) print('=' * 30 + ' Dependency Checks Failed ' + '=' * 30) print(f'Target: {gn_target}') print('Direct classpath is incomplete. To fix, add deps on:') for missing_target_names, deps_to in missing_targets.items(): if len(missing_target_names) > 1: print(f' * One of {", ".join(missing_target_names)}') else: print(f' * {missing_target_names[0]}') for dep_to in deps_to: dep_from = missing_classes[dep_to] print(f' ** {dep_to} (needed by {dep_from})') print('\nHint: Run the following command to add the missing deps:') print(f' {shlex.join(cmd)}\n') if warnings_as_errors: sys.exit(1) if not auto_add_deps: print_and_maybe_exit() else: failed = False try: stdout = build_utils.CheckOutput(cmd, cwd=build_utils.DIR_SOURCE_ROOT, fail_on_output=warnings_as_errors) if f'Unable to find {gn_target}' in stdout: # This can happen if a target's deps are stored in a variable instead # of a list and then simply assigned: `deps = deps_variable`. These # need to be manually added to the `deps_variable`. failed = True except build_utils.CalledProcessError as e: if NO_VALID_GN_STR in e.output: failed = True else: raise build_file_path = dep_utils.GnTargetToBuildFilePath(gn_target) if failed: print(f'Unable to auto-add missing dep(s) to {build_file_path}.') print_and_maybe_exit() else: gn_target_name = gn_target.split(':', 1)[-1] print(f'Successfully updated "{gn_target_name}" in {build_file_path} ' f'with missing direct deps: {missing_deps}') def main(argv): build_utils.InitLogging('BYTECODE_PROCESSOR_DEBUG') argv = build_utils.ExpandFileArgs(argv[1:]) parser = argparse.ArgumentParser() parser.add_argument('--target-name', help='Fully qualified GN target name.') parser.add_argument('--use-build-server', action='store_true', help='Always use the build server.') parser.add_argument('--gn-target', required=True) parser.add_argument('--input-jar', required=True) parser.add_argument('--direct-classpath-jars') parser.add_argument('--sdk-classpath-jars') parser.add_argument('--full-classpath-jars') parser.add_argument('--full-classpath-gn-targets') parser.add_argument('--chromium-output-dir') parser.add_argument('--stamp') parser.add_argument('--warnings-as-errors', action='store_true', help='Treat all warnings as errors.') parser.add_argument( '--auto-add-deps', action='store_true', help='Attempt to automatically add missing deps to the corresponding ' 'BUILD.gn file.') args = parser.parse_args(argv) if server_utils.MaybeRunCommand(name=args.target_name, argv=sys.argv, stamp_file=args.stamp, force=args.use_build_server): return args.sdk_classpath_jars = action_helpers.parse_gn_list( args.sdk_classpath_jars) args.direct_classpath_jars = action_helpers.parse_gn_list( args.direct_classpath_jars) args.full_classpath_jars = action_helpers.parse_gn_list( args.full_classpath_jars) args.full_classpath_gn_targets = [ dep_utils.ReplaceGmsPackageIfNeeded(t) for t in action_helpers.parse_gn_list(args.full_classpath_gn_targets) ] logging.info('Processed args for %s, starting direct classpath check.', args.target_name) _EnsureDirectClasspathIsComplete( input_jar=args.input_jar, gn_target=args.gn_target, output_dir=args.chromium_output_dir, sdk_classpath_jars=args.sdk_classpath_jars, direct_classpath_jars=args.direct_classpath_jars, full_classpath_jars=args.full_classpath_jars, full_classpath_gn_targets=args.full_classpath_gn_targets, warnings_as_errors=args.warnings_as_errors, auto_add_deps=args.auto_add_deps, ) logging.info('Check completed.') if args.stamp: build_utils.Touch(args.stamp) if __name__ == '__main__': sys.exit(main(sys.argv))