xref: /aosp_15_r20/external/angle/build/android/gyp/util/jar_utils.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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 subprocess
9import zipfile
10from typing import List, Optional, Union
11
12_SRC_PATH = pathlib.Path(__file__).resolve().parents[4]
13_JDEPS_PATH = _SRC_PATH / 'third_party/jdk/current/bin/jdeps'
14
15_IGNORED_JAR_PATHS = [
16    # This matches org_ow2_asm_asm_commons and org_ow2_asm_asm_analysis, both of
17    # which fail jdeps (not sure why), see: https://crbug.com/348423879
18    'third_party/android_deps/cipd/libs/org_ow2_asm_asm',
19]
20
21def _should_ignore(jar_path: pathlib.Path) -> bool:
22  for ignored_jar_path in _IGNORED_JAR_PATHS:
23    if ignored_jar_path in str(jar_path):
24      return True
25  return False
26
27
28def run_jdeps(filepath: pathlib.Path,
29              *,
30              jdeps_path: pathlib.Path = _JDEPS_PATH,
31              verbose: bool = False) -> Optional[str]:
32  """Runs jdeps on the given filepath and returns the output."""
33  if not filepath.exists() or _should_ignore(filepath):
34    # Some __compile_java targets do not generate a .jar file, skipping these
35    # does not affect correctness.
36    return None
37
38  cmd = [
39      str(jdeps_path),
40      '-verbose:class',
41      '--multi-release',  # Some jars support multiple JDK releases.
42      'base',
43      str(filepath),
44  ]
45
46  if verbose:
47    logging.debug('Starting %s', filepath)
48  try:
49    return subprocess.run(
50        cmd,
51        check=True,
52        text=True,
53        capture_output=True,
54    ).stdout
55  except subprocess.CalledProcessError as e:
56    # Pack all the information into the error message since that is the last
57    # thing visible in the output.
58    raise RuntimeError(f'\nFilepath:\n{filepath}\ncmd:\n{" ".join(cmd)}\n'
59                       f'stdout:\n{e.stdout}\nstderr:{e.stderr}\n') from e
60  finally:
61    if verbose:
62      logging.debug('Finished %s', filepath)
63
64
65def extract_full_class_names_from_jar(
66    jar_path: Union[str, pathlib.Path]) -> List[str]:
67  """Returns set of fully qualified class names in passed-in jar."""
68  out = set()
69  with zipfile.ZipFile(jar_path) as z:
70    for zip_entry_name in z.namelist():
71      if not zip_entry_name.endswith('.class'):
72        continue
73      # Remove .class suffix
74      full_java_class = zip_entry_name[:-6]
75
76      # Remove inner class names after the first $.
77      full_java_class = full_java_class.replace('/', '.')
78      dollar_index = full_java_class.find('$')
79      if dollar_index >= 0:
80        full_java_class = full_java_class[0:dollar_index]
81
82      out.add(full_java_class)
83  return sorted(out)
84
85
86def parse_full_java_class(source_path: pathlib.Path) -> str:
87  """Guess the fully qualified class name from the path to the source file."""
88  if source_path.suffix not in ('.java', '.kt'):
89    logging.warning('"%s" does not end in .java or .kt.', source_path)
90    return ''
91
92  directory_path = source_path.parent
93  package_list_reversed = []
94  for part in reversed(directory_path.parts):
95    if part == 'java':
96      break
97    package_list_reversed.append(part)
98    if part in ('com', 'org'):
99      break
100  else:
101    logging.debug(
102        'File %s not in a subdir of "org" or "com", cannot detect '
103        'package heuristically.', source_path)
104    return ''
105
106  package = '.'.join(reversed(package_list_reversed))
107  class_name = source_path.stem
108  return f'{package}.{class_name}'
109