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