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 return matches 111 112 def _entries_for(self, class_name) -> List[ClassEntry]: 113 return sorted(self._class_index[class_name]) 114 115 def _index_root(self) -> Dict[str, Set[ClassEntry]]: 116 """Create the class to target index.""" 117 logging.debug('Running list_java_targets.py...') 118 list_java_targets_command = [ 119 'build/android/list_java_targets.py', '--gn-labels', 120 '--print-build-config-paths', 121 f'--output-directory={self._abs_build_output_dir}' 122 ] 123 if self._should_build: 124 list_java_targets_command += ['--build'] 125 126 list_java_targets_run = subprocess.run(list_java_targets_command, 127 cwd=_SRC_PATH, 128 capture_output=True, 129 text=True, 130 check=True) 131 logging.debug('... done.') 132 133 # Parse output of list_java_targets.py into BuildConfig objects. 134 path_to_build_config: Dict[str, BuildConfig] = {} 135 target_lines = list_java_targets_run.stdout.splitlines() 136 for target_line in target_lines: 137 # Skip empty lines 138 if not target_line: 139 continue 140 141 target_line_parts = target_line.split(': ') 142 assert len(target_line_parts) == 2, target_line_parts 143 target_name, build_config_path = target_line_parts 144 145 if not os.path.exists(build_config_path): 146 assert not self._should_build 147 continue 148 149 with open(build_config_path) as build_config_contents: 150 build_config_json: Dict = json.load(build_config_contents) 151 deps_info = build_config_json['deps_info'] 152 153 # Checking the library type here instead of in list_java_targets.py avoids 154 # reading each .build_config file twice. 155 if deps_info['type'] not in ('java_library', 'group'): 156 continue 157 158 relpath = os.path.relpath(build_config_path, self._abs_build_output_dir) 159 preferred_dep = bool(deps_info.get('preferred_dep')) 160 is_group = bool(deps_info.get('type') == 'group') 161 dependent_config_paths = deps_info.get('deps_configs', []) 162 full_class_names = self._compute_full_class_names_for_build_config( 163 deps_info) 164 build_config = BuildConfig(relpath=relpath, 165 target_name=target_name, 166 is_group=is_group, 167 preferred_dep=preferred_dep, 168 dependent_config_paths=dependent_config_paths, 169 full_class_names=full_class_names) 170 path_to_build_config[relpath] = build_config 171 172 # From GN's perspective, depending on a java group is the same as depending 173 # on all of its deps directly, since groups are collapsed in 174 # write_build_config.py. Thus, collect all the java files in a java group's 175 # deps (recursing into other java groups) and set that as the java group's 176 # list of classes. 177 for build_config in path_to_build_config.values(): 178 if build_config.is_group: 179 for dep_build_config in build_config.all_dependent_configs( 180 path_to_build_config): 181 build_config.full_class_names.update( 182 dep_build_config.full_class_names) 183 184 class_index = collections.defaultdict(set) 185 for build_config in path_to_build_config.values(): 186 for full_class_name in build_config.full_class_names: 187 class_index[full_class_name].add( 188 ClassEntry(full_class_name=full_class_name, 189 target=build_config.target_name, 190 preferred_dep=build_config.preferred_dep)) 191 192 return class_index 193 194 def _compute_full_class_names_for_build_config(self, 195 deps_info: Dict) -> Set[str]: 196 """Returns set of fully qualified class names for build config.""" 197 198 full_class_names = set() 199 200 # Read the location of the target_sources_file from the build_config 201 sources_path = deps_info.get('target_sources_file') 202 if sources_path: 203 # Read the target_sources_file, indexing the classes found 204 with open(self._abs_build_output_dir / sources_path) as sources_contents: 205 for source_line in sources_contents: 206 source_path = pathlib.Path(source_line.strip()) 207 java_class = jar_utils.parse_full_java_class(source_path) 208 if java_class: 209 full_class_names.add(java_class) 210 211 # |unprocessed_jar_path| is set for prebuilt targets. (ex: 212 # android_aar_prebuilt()) 213 # |unprocessed_jar_path| might be set but not exist if not all targets have 214 # been built. 215 unprocessed_jar_path = deps_info.get('unprocessed_jar_path') 216 if unprocessed_jar_path: 217 abs_unprocessed_jar_path = (self._abs_build_output_dir / 218 unprocessed_jar_path) 219 if abs_unprocessed_jar_path.exists(): 220 # Normalize path but do not follow symlink if .jar is symlink. 221 abs_unprocessed_jar_path = (abs_unprocessed_jar_path.parent.resolve() / 222 abs_unprocessed_jar_path.name) 223 224 full_class_names.update( 225 jar_utils.extract_full_class_names_from_jar( 226 self._abs_build_output_dir, abs_unprocessed_jar_path)) 227 228 return full_class_names 229 230 231def ReplaceGmsPackageIfNeeded(target_name: str) -> str: 232 if target_name.startswith( 233 ('//third_party/android_deps:google_play_services_', 234 '//clank/third_party/google3:google_play_services_')): 235 return f'$google_play_services_package:{target_name.split(":")[1]}' 236 return target_name 237 238 239def DisambiguateDeps(class_entries: List[ClassEntry]): 240 def filter_if_not_empty(entries, filter_func): 241 filtered_entries = [e for e in entries if filter_func(e)] 242 return filtered_entries or entries 243 244 # When some deps are preferred, ignore all other potential deps. 245 class_entries = filter_if_not_empty(class_entries, lambda e: e.preferred_dep) 246 247 # E.g. javax_annotation_jsr250_api_java. 248 class_entries = filter_if_not_empty(class_entries, 249 lambda e: 'jsr' in e.target) 250 251 # Avoid suggesting subtargets when regular targets exist. 252 class_entries = filter_if_not_empty(class_entries, 253 lambda e: '__' not in e.target) 254 255 # Swap out GMS package names if needed. 256 class_entries = [ 257 dataclasses.replace(e, target=ReplaceGmsPackageIfNeeded(e.target)) 258 for e in class_entries 259 ] 260 261 # Convert to dict and then use list to get the keys back to remove dups and 262 # keep order the same as before. 263 class_entries = list({e: True for e in class_entries}) 264 265 return class_entries 266