dep_utils.py (11433B)
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 6 from __future__ import annotations 7 import collections 8 9 import dataclasses 10 import json 11 import logging 12 import os 13 import pathlib 14 import subprocess 15 import sys 16 from typing import Dict, Iterator, List, Set 17 18 from util import jar_utils 19 20 _SRC_PATH = pathlib.Path(__file__).resolve().parents[4] 21 22 sys.path.append(str(_SRC_PATH / 'build/android')) 23 # Import list_java_targets so that the dependency is found by print_python_deps. 24 import list_java_targets 25 26 27 @dataclasses.dataclass(frozen=True) 28 class 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 52 class 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 75 class 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 239 def 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 252 def 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 263 def 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 271 def 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