Path: blob/main_old/tools/android/modularization/convenience/lookup_dep.py
1695 views
#!/usr/bin/env python31# Copyright 2021 The Chromium Authors. All rights reserved.2# Use of this source code is governed by a BSD-style license that can be3# found in the LICENSE file.4r'''Finds which build target(s) contain a particular Java class.56This is a utility script for finding out which build target dependency needs to7be added to import a given Java class.89It is a best-effort script.1011Example:1213Find build target with class FooUtil:14tools/android/modularization/convenience/lookup_dep.py FooUtil15'''1617import argparse18import collections19import dataclasses20import json21import logging22import os23import pathlib24import subprocess25import sys26import zipfile27from typing import Dict, List, Set2829_SRC_DIR = pathlib.Path(__file__).parents[4].resolve()3031sys.path.append(str(_SRC_DIR / 'build' / 'android'))32from pylib import constants3334# Import list_java_targets so that the dependency is found by print_python_deps.35import list_java_targets363738def main():39arg_parser = argparse.ArgumentParser(40description='Finds which build target contains a particular Java class.')4142arg_parser.add_argument('-C', '--output-directory', help='Build output directory.')43arg_parser.add_argument('--build', action='store_true', help='Build all .build_config files.')44arg_parser.add_argument('classes', nargs='+', help='Java classes to search for')45arg_parser.add_argument('-v', '--verbose', action='store_true', help='Verbose logging.')4647arguments = arg_parser.parse_args()4849logging.basicConfig(50level=logging.DEBUG if arguments.verbose else logging.WARNING,51format='%(asctime)s.%(msecs)03d %(levelname).1s %(message)s',52datefmt='%H:%M:%S')5354if arguments.output_directory:55constants.SetOutputDirectory(arguments.output_directory)56constants.CheckOutputDirectory()57abs_out_dir: pathlib.Path = pathlib.Path(constants.GetOutDirectory()).resolve()5859index = ClassLookupIndex(abs_out_dir, arguments.build)60matches = {c: index.match(c) for c in arguments.classes}6162if not arguments.build:63# Try finding match without building because it is faster.64for class_name, match_list in matches.items():65if len(match_list) == 0:66arguments.build = True67break68if arguments.build:69index = ClassLookupIndex(abs_out_dir, True)70matches = {c: index.match(c) for c in arguments.classes}7172if not arguments.build:73print('Showing potentially stale results. Run lookup.dep.py with --build '74'(slower) to build any unbuilt GN targets and get full results.')75print()7677for (class_name, class_entries) in matches.items():78if not class_entries:79print(f'Could not find build target for class "{class_name}"')80elif len(class_entries) == 1:81class_entry = class_entries[0]82print(f'Class {class_entry.full_class_name} found:')83print(f' "{class_entry.target}"')84else:85print(f'Multiple targets with classes that match "{class_name}":')86print()87for class_entry in class_entries:88print(f' "{class_entry.target}"')89print(f' contains {class_entry.full_class_name}')90print()919293@dataclasses.dataclass(frozen=True)94class ClassEntry:95"""An assignment of a Java class to a build target."""96full_class_name: str97target: str9899100class ClassLookupIndex:101"""A map from full Java class to its build targets.102103A class might be in multiple targets if it's bytecode rewritten."""104105def __init__(self, abs_build_output_dir: pathlib.Path, should_build: bool):106self._abs_build_output_dir = abs_build_output_dir107self._should_build = should_build108self._class_index = self._index_root()109110def match(self, search_string: str) -> List[ClassEntry]:111"""Get class/target entries where the class matches search_string"""112# Priority 1: Exact full matches113if search_string in self._class_index:114return self._entries_for(search_string)115116# Priority 2: Match full class name (any case), if it's a class name117matches = []118lower_search_string = search_string.lower()119if '.' not in lower_search_string:120for full_class_name in self._class_index:121package_and_class = full_class_name.rsplit('.', 1)122if len(package_and_class) < 2:123continue124class_name = package_and_class[1]125class_lower = class_name.lower()126if class_lower == lower_search_string:127matches.extend(self._entries_for(full_class_name))128if matches:129return matches130131# Priority 3: Match anything132for full_class_name in self._class_index:133if lower_search_string in full_class_name.lower():134matches.extend(self._entries_for(full_class_name))135136return matches137138def _entries_for(self, class_name) -> List[ClassEntry]:139return [ClassEntry(class_name, target) for target in self._class_index.get(class_name)]140141def _index_root(self) -> Dict[str, List[str]]:142"""Create the class to target index."""143logging.debug('Running list_java_targets.py...')144list_java_targets_command = [145'build/android/list_java_targets.py', '--gn-labels', '--print-build-config-paths',146f'--output-directory={self._abs_build_output_dir}'147]148if self._should_build:149list_java_targets_command += ['--build']150151list_java_targets_run = subprocess.run(152list_java_targets_command, cwd=_SRC_DIR, capture_output=True, text=True, check=True)153logging.debug('... done.')154155# Parse output of list_java_targets.py with mapping of build_target to156# build_config157root_build_targets = list_java_targets_run.stdout.split('\n')158class_index = collections.defaultdict(list)159for target_line in root_build_targets:160# Skip empty lines161if not target_line:162continue163164target_line_parts = target_line.split(': ')165assert len(target_line_parts) == 2, target_line_parts166target, build_config_path = target_line_parts167168if not os.path.exists(build_config_path):169assert not self._should_build170continue171172with open(build_config_path) as build_config_contents:173build_config: Dict = json.load(build_config_contents)174deps_info = build_config['deps_info']175# Checking the library type here instead of in list_java_targets.py avoids176# reading each .build_config file twice.177if deps_info['type'] != 'java_library':178continue179180target = self._compute_toplevel_target(target)181full_class_names = self._compute_full_class_names_for_build_config(deps_info)182for full_class_name in full_class_names:183class_index[full_class_name].append(target)184185return class_index186187@staticmethod188def _compute_toplevel_target(target: str) -> str:189"""Computes top level target from the passed-in sub-target."""190if target.endswith('_java'):191return target192193# Handle android_aar_prebuilt() sub targets.194index = target.find('_java__subjar')195if index >= 0:196return target[0:index + 5]197index = target.find('_java__classes')198if index >= 0:199return target[0:index + 5]200201return target202203def _compute_full_class_names_for_build_config(self, deps_info: Dict) -> Set[str]:204"""Returns set of fully qualified class names for build config."""205206full_class_names = set()207208# Read the location of the java_sources_file from the build_config209sources_path = deps_info.get('java_sources_file')210if sources_path:211# Read the java_sources_file, indexing the classes found212with open(self._abs_build_output_dir / sources_path) as sources_contents:213for source_line in sources_contents:214source_path = pathlib.Path(source_line.strip())215java_class = self._parse_full_java_class(source_path)216if java_class:217full_class_names.add(java_class)218219# |unprocessed_jar_path| is set for prebuilt targets. (ex:220# android_aar_prebuilt())221# |unprocessed_jar_path| might be set but not exist if not all targets have222# been built.223unprocessed_jar_path = deps_info.get('unprocessed_jar_path')224if unprocessed_jar_path:225abs_unprocessed_jar_path = (self._abs_build_output_dir / unprocessed_jar_path)226if abs_unprocessed_jar_path.exists():227# Normalize path but do not follow symlink if .jar is symlink.228abs_unprocessed_jar_path = (229abs_unprocessed_jar_path.parent.resolve() / abs_unprocessed_jar_path.name)230231full_class_names.update(232self._extract_full_class_names_from_jar(self._abs_build_output_dir,233abs_unprocessed_jar_path))234235return full_class_names236237@staticmethod238def _extract_full_class_names_from_jar(abs_build_output_dir: pathlib.Path,239abs_jar_path: pathlib.Path) -> Set[str]:240"""Returns set of fully qualified class names in passed-in jar."""241out = set()242jar_namelist = ClassLookupIndex._read_jar_namelist(abs_build_output_dir, abs_jar_path)243for zip_entry_name in jar_namelist:244if not zip_entry_name.endswith('.class'):245continue246# Remove .class suffix247full_java_class = zip_entry_name[:-6]248249full_java_class = full_java_class.replace('/', '.')250dollar_index = full_java_class.find('$')251if dollar_index >= 0:252full_java_class[0:dollar_index]253254out.add(full_java_class)255return out256257@staticmethod258def _read_jar_namelist(abs_build_output_dir: pathlib.Path,259abs_jar_path: pathlib.Path) -> List[str]:260"""Returns list of jar members by name."""261262# Caching namelist speeds up lookup_dep.py runtime by 1.5s.263cache_path = abs_jar_path.with_suffix(abs_jar_path.suffix + '.namelist_cache')264if (not ClassLookupIndex._is_path_relative_to(abs_jar_path, abs_build_output_dir)):265cache_path = (abs_build_output_dir / 'gen' / cache_path.relative_to(_SRC_DIR))266if (cache_path.exists() and os.path.getmtime(cache_path) > os.path.getmtime(abs_jar_path)):267with open(cache_path) as f:268return [s.strip() for s in f.readlines()]269270with zipfile.ZipFile(abs_jar_path) as z:271namelist = z.namelist()272273cache_path.parent.mkdir(parents=True, exist_ok=True)274with open(cache_path, 'w') as f:275f.write('\n'.join(namelist))276277return namelist278279@staticmethod280def _is_path_relative_to(path: pathlib.Path, other: pathlib.Path) -> bool:281# PurePath.is_relative_to() was introduced in Python 3.9282resolved_path = path.resolve()283resolved_other = other.resolve()284return str(resolved_path).startswith(str(resolved_other))285286@staticmethod287def _parse_full_java_class(source_path: pathlib.Path) -> str:288"""Guess the fully qualified class name from the path to the source file."""289if source_path.suffix != '.java':290logging.warning(f'"{source_path}" does not have the .java suffix')291return None292293directory_path: pathlib.Path = source_path.parent294package_list_reversed = []295for part in reversed(directory_path.parts):296if part == 'java':297break298package_list_reversed.append(part)299if part in ('com', 'org'):300break301else:302logging.debug(f'File {source_path} not in a subdir of "org" or "com", '303'cannot detect package heuristically.')304return None305306package = '.'.join(reversed(package_list_reversed))307class_name = source_path.stem308return f'{package}.{class_name}'309310311if __name__ == '__main__':312main()313314315