tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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