xref: /aosp_15_r20/external/cronet/build/android/gyp/util/dep_utils.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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