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 '-filter:none', # Necessary to include intra-package deps. 42 '--multi-release', # Some jars support multiple JDK releases. 43 'base', 44 str(filepath), 45 ] 46 47 if verbose: 48 logging.debug('Starting %s', filepath) 49 try: 50 return subprocess.run( 51 cmd, 52 check=True, 53 text=True, 54 capture_output=True, 55 ).stdout 56 except subprocess.CalledProcessError as e: 57 # Pack all the information into the error message since that is the last 58 # thing visible in the output. 59 raise RuntimeError(f'\nFilepath:\n{filepath}\ncmd:\n{" ".join(cmd)}\n' 60 f'stdout:\n{e.stdout}\nstderr:{e.stderr}\n') from e 61 finally: 62 if verbose: 63 logging.debug('Finished %s', filepath) 64 65 66def extract_full_class_names_from_jar( 67 jar_path: Union[str, pathlib.Path]) -> List[str]: 68 """Returns set of fully qualified class names in passed-in jar.""" 69 out = set() 70 with zipfile.ZipFile(jar_path) as z: 71 for zip_entry_name in z.namelist(): 72 if not zip_entry_name.endswith('.class'): 73 continue 74 # Remove .class suffix 75 full_java_class = zip_entry_name[:-6] 76 77 # Remove inner class names after the first $. 78 full_java_class = full_java_class.replace('/', '.') 79 dollar_index = full_java_class.find('$') 80 if dollar_index >= 0: 81 full_java_class = full_java_class[0:dollar_index] 82 83 out.add(full_java_class) 84 return sorted(out) 85 86 87def parse_full_java_class(source_path: pathlib.Path) -> str: 88 """Guess the fully qualified class name from the path to the source file.""" 89 if source_path.suffix not in ('.java', '.kt'): 90 logging.warning('"%s" does not end in .java or .kt.', source_path) 91 return '' 92 93 directory_path = source_path.parent 94 package_list_reversed = [] 95 for part in reversed(directory_path.parts): 96 if part == 'java': 97 break 98 package_list_reversed.append(part) 99 if part in ('com', 'org'): 100 break 101 else: 102 logging.debug( 103 'File %s not in a subdir of "org" or "com", cannot detect ' 104 'package heuristically.', source_path) 105 return '' 106 107 package = '.'.join(reversed(package_list_reversed)) 108 class_name = source_path.stem 109 return f'{package}.{class_name}' 110