xref: /aosp_15_r20/external/cronet/build/android/gyp/util/jar_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 to run tools over jars and cache their output."""
5
6import logging
7import pathlib
8import zipfile
9from typing import List, Optional, Union
10
11from util import build_utils
12
13_SRC_PATH = pathlib.Path(__file__).resolve().parents[4]
14_JDEPS_PATH = _SRC_PATH / 'third_party/jdk/current/bin/jdeps'
15
16_IGNORED_JAR_PATHS = [
17    # This matches org_ow2_asm_asm_commons and org_ow2_asm_asm_analysis, both of
18    # which fail jdeps (not sure why).
19    'third_party/android_deps/libs/org_ow2_asm_asm',
20]
21
22
23def _is_relative_to(path: pathlib.Path, other_path: pathlib.Path):
24  """This replicates pathlib.Path.is_relative_to.
25
26    Since bots still run python3.8, they do not have access to is_relative_to,
27    which was introduced in python3.9.
28    """
29  try:
30    path.relative_to(other_path)
31    return True
32  except ValueError:
33    # This error is expected when path is not a subpath of other_path.
34    return False
35
36
37def _should_ignore(jar_path: pathlib.Path) -> bool:
38  for ignored_jar_path in _IGNORED_JAR_PATHS:
39    if ignored_jar_path in str(jar_path):
40      return True
41  return False
42
43
44def run_jdeps(filepath: pathlib.Path,
45              *,
46              jdeps_path: pathlib.Path = _JDEPS_PATH) -> Optional[str]:
47  """Runs jdeps on the given filepath and returns the output."""
48  if not filepath.exists() or _should_ignore(filepath):
49    # Some __compile_java targets do not generate a .jar file, skipping these
50    # does not affect correctness.
51    return None
52
53  return build_utils.CheckOutput([
54      str(jdeps_path),
55      '-verbose:class',
56      '--multi-release',  # Some jars support multiple JDK releases.
57      'base',
58      str(filepath),
59  ])
60
61
62def extract_full_class_names_from_jar(
63    jar_path: Union[str, pathlib.Path]) -> List[str]:
64  """Returns set of fully qualified class names in passed-in jar."""
65  out = set()
66  with zipfile.ZipFile(jar_path) as z:
67    for zip_entry_name in z.namelist():
68      if not zip_entry_name.endswith('.class'):
69        continue
70      # Remove .class suffix
71      full_java_class = zip_entry_name[:-6]
72
73      # Remove inner class names after the first $.
74      full_java_class = full_java_class.replace('/', '.')
75      dollar_index = full_java_class.find('$')
76      if dollar_index >= 0:
77        full_java_class = full_java_class[0:dollar_index]
78
79      out.add(full_java_class)
80  return sorted(out)
81
82
83def parse_full_java_class(source_path: pathlib.Path) -> str:
84  """Guess the fully qualified class name from the path to the source file."""
85  if source_path.suffix not in ('.java', '.kt'):
86    logging.warning('"%s" does not end in .java or .kt.', source_path)
87    return ''
88
89  directory_path = source_path.parent
90  package_list_reversed = []
91  for part in reversed(directory_path.parts):
92    if part == 'java':
93      break
94    package_list_reversed.append(part)
95    if part in ('com', 'org'):
96      break
97  else:
98    logging.debug(
99        'File %s not in a subdir of "org" or "com", cannot detect '
100        'package heuristically.', source_path)
101    return ''
102
103  package = '.'.join(reversed(package_list_reversed))
104  class_name = source_path.stem
105  return f'{package}.{class_name}'
106