• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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