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