1# Copyright 2023 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Methods for managing deps based on build_config.json files.""" 5 6from __future__ import annotations 7import collections 8 9import dataclasses 10import json 11import logging 12import os 13import pathlib 14import subprocess 15import sys 16from typing import Dict, Iterator, List, Set 17 18from util import jar_utils 19 20_SRC_PATH = pathlib.Path(__file__).resolve().parents[4] 21 22sys.path.append(str(_SRC_PATH / 'build/android')) 23# Import list_java_targets so that the dependency is found by print_python_deps. 24import list_java_targets 25 26 27@dataclasses.dataclass(frozen=True) 28class ClassEntry: 29 """An assignment of a Java class to a build target.""" 30 full_class_name: str 31 target: str 32 preferred_dep: bool 33 34 def __lt__(self, other: 'ClassEntry'): 35 # Prefer canonical targets first. 36 if self.preferred_dep and not other.preferred_dep: 37 return True 38 # Prefer targets without __ in the name. Usually double underscores are used 39 # for internal subtargets and not top level targets. 40 if '__' not in self.target and '__' in other.target: 41 return True 42 # Prefer shorter target names first since they are usually the correct ones. 43 if len(self.target) < len(other.target): 44 return True 45 if len(self.target) > len(other.target): 46 return False 47 # Use string comparison to get a stable ordering of equal-length names. 48 return self.target < other.target 49 50 51@dataclasses.dataclass 52class BuildConfig: 53 """Container for information from a build config.""" 54 target_name: str 55 relpath: str 56 is_group: bool 57 preferred_dep: bool 58 dependent_config_paths: List[str] 59 full_class_names: Set[str] 60 61 def all_dependent_configs( 62 self, 63 path_to_configs: Dict[str, 'BuildConfig'], 64 ) -> Iterator['BuildConfig']: 65 for path in self.dependent_config_paths: 66 dep_build_config = path_to_configs.get(path) 67 # This can happen when a java group depends on non-java targets. 68 if dep_build_config is None: 69 continue 70 yield dep_build_config 71 if dep_build_config.is_group: 72 yield from dep_build_config.all_dependent_configs(path_to_configs) 73 74 75class ClassLookupIndex: 76 """A map from full Java class to its build targets. 77 78 A class might be in multiple targets if it's bytecode rewritten.""" 79 def __init__(self, build_output_dir: pathlib.Path, should_build: bool): 80 self._abs_build_output_dir = build_output_dir.resolve().absolute() 81 self._should_build = should_build 82 self._class_index = self._index_root() 83 84 def match(self, search_string: str) -> List[ClassEntry]: 85 """Get class/target entries where the class matches search_string""" 86 # Priority 1: Exact full matches 87 if search_string in self._class_index: 88 return self._entries_for(search_string) 89 90 # Priority 2: Match full class name (any case), if it's a class name 91 matches = [] 92 lower_search_string = search_string.lower() 93 if '.' not in lower_search_string: 94 for full_class_name in self._class_index: 95 package_and_class = full_class_name.rsplit('.', 1) 96 if len(package_and_class) < 2: 97 continue 98 class_name = package_and_class[1] 99 class_lower = class_name.lower() 100 if class_lower == lower_search_string: 101 matches.extend(self._entries_for(full_class_name)) 102 if matches: 103 return matches 104 105 # Priority 3: Match anything 106 for full_class_name in self._class_index: 107 if lower_search_string in full_class_name.lower(): 108 matches.extend(self._entries_for(full_class_name)) 109 110 # Priority 4: Match parent class when no matches and it's an inner class. 111 if not matches: 112 components = search_string.rsplit('.', 2) 113 if len(components) == 3: 114 package, outer_class, inner_class = components 115 if outer_class[0].isupper() and inner_class[0].isupper(): 116 matches.extend(self.match(f'{package}.{outer_class}')) 117 118 return matches 119 120 def _entries_for(self, class_name) -> List[ClassEntry]: 121 return sorted(self._class_index[class_name]) 122 123 def _index_root(self) -> Dict[str, Set[ClassEntry]]: 124 """Create the class to target index.""" 125 logging.debug('Running list_java_targets.py...') 126 list_java_targets_command = [ 127 'build/android/list_java_targets.py', '--gn-labels', 128 '--print-build-config-paths', 129 f'--output-directory={self._abs_build_output_dir}' 130 ] 131 if self._should_build: 132 list_java_targets_command += ['--build'] 133 134 list_java_targets_run = subprocess.run(list_java_targets_command, 135 cwd=_SRC_PATH, 136 capture_output=True, 137 text=True, 138 check=True) 139 logging.debug('... done.') 140 141 # Parse output of list_java_targets.py into BuildConfig objects. 142 path_to_build_config: Dict[str, BuildConfig] = {} 143 target_lines = list_java_targets_run.stdout.splitlines() 144 for target_line in target_lines: 145 # Skip empty lines 146 if not target_line: 147 continue 148 149 target_line_parts = target_line.split(': ') 150 assert len(target_line_parts) == 2, target_line_parts 151 target_name, build_config_path = target_line_parts 152 153 if not os.path.exists(build_config_path): 154 assert not self._should_build 155 continue 156 157 with open(build_config_path) as build_config_contents: 158 build_config_json: Dict = json.load(build_config_contents) 159 deps_info = build_config_json['deps_info'] 160 161 # Checking the library type here instead of in list_java_targets.py avoids 162 # reading each .build_config file twice. 163 if deps_info['type'] not in ('java_library', 'group'): 164 continue 165 166 relpath = os.path.relpath(build_config_path, self._abs_build_output_dir) 167 preferred_dep = bool(deps_info.get('preferred_dep')) 168 is_group = bool(deps_info.get('type') == 'group') 169 dependent_config_paths = deps_info.get('deps_configs', []) 170 full_class_names = self._compute_full_class_names_for_build_config( 171 deps_info) 172 build_config = BuildConfig(relpath=relpath, 173 target_name=target_name, 174 is_group=is_group, 175 preferred_dep=preferred_dep, 176 dependent_config_paths=dependent_config_paths, 177 full_class_names=full_class_names) 178 path_to_build_config[relpath] = build_config 179 180 # From GN's perspective, depending on a java group is the same as depending 181 # on all of its deps directly, since groups are collapsed in 182 # write_build_config.py. Thus, collect all the java files in a java group's 183 # deps (recursing into other java groups) and set that as the java group's 184 # list of classes. 185 for build_config in path_to_build_config.values(): 186 if build_config.is_group: 187 for dep_build_config in build_config.all_dependent_configs( 188 path_to_build_config): 189 build_config.full_class_names.update( 190 dep_build_config.full_class_names) 191 192 class_index = collections.defaultdict(set) 193 for build_config in path_to_build_config.values(): 194 for full_class_name in build_config.full_class_names: 195 class_index[full_class_name].add( 196 ClassEntry(full_class_name=full_class_name, 197 target=build_config.target_name, 198 preferred_dep=build_config.preferred_dep)) 199 200 return class_index 201 202 def _compute_full_class_names_for_build_config(self, 203 deps_info: Dict) -> Set[str]: 204 """Returns set of fully qualified class names for build config.""" 205 206 full_class_names = set() 207 208 # Read the location of the target_sources_file from the build_config 209 sources_path = deps_info.get('target_sources_file') 210 if sources_path: 211 # Read the target_sources_file, indexing the classes found 212 with open(self._abs_build_output_dir / sources_path) as sources_contents: 213 for source_line in sources_contents: 214 source_path = pathlib.Path(source_line.strip()) 215 java_class = jar_utils.parse_full_java_class(source_path) 216 if java_class: 217 full_class_names.add(java_class) 218 219 # |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 have 222 # been built. 223 unprocessed_jar_path = deps_info.get('unprocessed_jar_path') 224 if unprocessed_jar_path: 225 abs_unprocessed_jar_path = (self._abs_build_output_dir / 226 unprocessed_jar_path) 227 if abs_unprocessed_jar_path.exists(): 228 # Normalize path but do not follow symlink if .jar is symlink. 229 abs_unprocessed_jar_path = (abs_unprocessed_jar_path.parent.resolve() / 230 abs_unprocessed_jar_path.name) 231 232 full_class_names.update( 233 jar_utils.extract_full_class_names_from_jar( 234 abs_unprocessed_jar_path)) 235 236 return full_class_names 237 238 239def GnTargetToBuildFilePath(gn_target: str): 240 """Returns the relative BUILD.gn file path for this target from src root.""" 241 assert gn_target.startswith('//'), f'Relative {gn_target} name not supported.' 242 ninja_target_name = gn_target[2:] 243 244 # Remove the colon at the end 245 colon_index = ninja_target_name.find(':') 246 if colon_index != -1: 247 ninja_target_name = ninja_target_name[:colon_index] 248 249 return os.path.join(ninja_target_name, 'BUILD.gn') 250 251 252def CreateAddDepsCommand(gn_target: str, missing_deps: List[str]) -> List[str]: 253 # Normalize chrome_public_apk__java to chrome_public_apk. 254 gn_target = gn_target.split('__', 1)[0] 255 256 build_file_path = GnTargetToBuildFilePath(gn_target) 257 return [ 258 'build/gn_editor', 'add', '--quiet', '--file', build_file_path, 259 '--target', gn_target, '--deps' 260 ] + missing_deps 261 262 263def ReplaceGmsPackageIfNeeded(target_name: str) -> str: 264 if target_name.startswith( 265 ('//third_party/android_deps:google_play_services_', 266 '//clank/third_party/google3:google_play_services_')): 267 return f'$google_play_services_package:{target_name.split(":")[1]}' 268 return target_name 269 270 271def DisambiguateDeps(class_entries: List[ClassEntry]): 272 def filter_if_not_empty(entries, filter_func): 273 filtered_entries = [e for e in entries if filter_func(e)] 274 return filtered_entries or entries 275 276 # When some deps are preferred, ignore all other potential deps. 277 class_entries = filter_if_not_empty(class_entries, lambda e: e.preferred_dep) 278 279 # E.g. javax_annotation_jsr250_api_java. 280 class_entries = filter_if_not_empty(class_entries, 281 lambda e: 'jsr' in e.target) 282 283 # Avoid suggesting subtargets when regular targets exist. 284 class_entries = filter_if_not_empty(class_entries, 285 lambda e: '__' not in e.target) 286 287 # Swap out GMS package names if needed. 288 class_entries = [ 289 dataclasses.replace(e, target=ReplaceGmsPackageIfNeeded(e.target)) 290 for e in class_entries 291 ] 292 293 # Convert to dict and then use list to get the keys back to remove dups and 294 # keep order the same as before. 295 class_entries = list({e: True for e in class_entries}) 296 297 return class_entries 298