1# 2# Copyright (C) 2024 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16""" This script collects all of the classes fully-qualified path provided in a JAR, applies 17post-processing then bundles that list in a java-compilable file.""" 18 19import argparse 20from pathlib import Path 21import re 22from zipfile import ZipFile 23 24 25def parse_arguments(argv): 26 parser = argparse.ArgumentParser() 27 parser.add_argument( 28 'jars', nargs='+', 29 help='Path to JARs. Multiple jars can be specified.') 30 parser.add_argument( 31 '--prefix', required=True, 32 help='Package prefix to use to append to the classes, ' 33 'for example "com.android.connectivity" (does not end with a dot).') 34 parser.add_argument( 35 '--output', required=True, help='Path to output java-compilable file.') 36 parser.add_argument( 37 '--prefix-excludes', action='append', default=[], 38 help='Path to files listing classes that should not have their package prefixed. ' 39 'Can be repeated to specify multiple files.' 40 'Each file should contain one full-match regex per line. Empty lines or lines ' 41 'starting with "#" are ignored.') 42 parser.add_argument( 43 '--excludes', action='append', default=[], 44 help='Path to files listing classes that should be excluded ' 45 'Can be repeated to specify multiple files.' 46 'Each file should contain one full-match regex per line. Empty lines or lines ' 47 'starting with "#" are ignored.') 48 parser.add_argument( 49 '--java-package', default="android.net.http", 50 help='The package name of the generated java file.') 51 return parser.parse_args(argv) 52 53 54def _list_jar_classes(jar): 55 with ZipFile(jar, 'r') as zip: 56 files = zip.namelist() 57 assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \ 58 'expected an intermediate zip of .class files' 59 class_suffix = '.class' 60 return [f.removesuffix(class_suffix).replace('/', '.') for f in files 61 if f.endswith(class_suffix) and not f.endswith('/package-info.class')] 62 63 64def _get_excludes(path): 65 out = [] 66 with open(path, 'r') as f: 67 for line in f: 68 stripped = line.strip() 69 if not stripped or stripped.startswith('#'): 70 continue 71 out.append(re.compile(stripped)) 72 return out 73 74 75def _write_classes_list(package_name, output_path, classes_list): 76 with open(output_path, 'w') as outfile: 77 outfile.write( 78 f""" 79package {package_name}; 80 81public class {Path(output_path).stem} {{ 82 public static final String[] ALL_CLASSES = {{{",".join(classes_list)}}}; 83}} 84""") 85 86 87def make_classes_list(args): 88 exclude_regexes = [] 89 for exclude_file in args.prefix_excludes: 90 exclude_regexes.extend(_get_excludes(exclude_file)) 91 92 exclude_collecting_regexes = [] 93 for exclude_file in args.excludes: 94 exclude_collecting_regexes.extend(_get_excludes(exclude_file)) 95 96 processed_classes = [] 97 for jar in args.jars: 98 jar_classes = _list_jar_classes(jar) 99 jar_classes.sort() 100 for clazz in jar_classes: 101 if any(r.fullmatch(clazz) for r in exclude_collecting_regexes): 102 continue 103 104 if (clazz.startswith(args.prefix + '.') or 105 any(r.fullmatch(clazz) for r in exclude_regexes)): 106 processed_classes.append(f'"{clazz}"') 107 else: 108 processed_classes.append(f'"{args.prefix}.{clazz}"') 109 return processed_classes 110 111 112def _main(): 113 # Pass in None to use argv 114 args = parse_arguments(None) 115 _write_classes_list(args.java_package, args.output, make_classes_list(args)) 116 117 118if __name__ == '__main__': 119 _main() 120