1# 2# Copyright (C) 2022 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 generates jarjar rule files to add a jarjar prefix to all classes, except those 17that are API, unsupported API or otherwise excluded.""" 18 19import argparse 20import io 21import re 22import subprocess 23from xml import sax 24from xml.sax.handler import ContentHandler 25from zipfile import ZipFile 26 27 28def parse_arguments(argv): 29 parser = argparse.ArgumentParser() 30 parser.add_argument( 31 'jars', nargs='+', 32 help='Path to pre-jarjar JAR. Multiple jars can be specified.') 33 parser.add_argument( 34 '--prefix', required=True, 35 help='Package prefix to use for jarjared classes, ' 36 'for example "com.android.connectivity" (does not end with a dot).') 37 parser.add_argument( 38 '--output', required=True, help='Path to output jarjar rules file.') 39 parser.add_argument( 40 '--apistubs', action='append', default=[], 41 help='Path to API stubs jar. Classes that are API will not be jarjared. Can be repeated to ' 42 'specify multiple jars.') 43 parser.add_argument( 44 '--unsupportedapi', 45 help='Column(:)-separated paths to UnsupportedAppUsage hidden API .txt lists. ' 46 'Classes that have UnsupportedAppUsage API will not be jarjared.') 47 parser.add_argument( 48 '--excludes', action='append', default=[], 49 help='Path to files listing classes that should not be jarjared. Can be repeated to ' 50 'specify multiple files.' 51 'Each file should contain one full-match regex per line. Empty lines or lines ' 52 'starting with "#" are ignored.') 53 return parser.parse_args(argv) 54 55 56def _list_toplevel_jar_classes(jar): 57 """List all classes in a .class .jar file that are not inner classes.""" 58 return {_get_toplevel_class(c) for c in _list_jar_classes(jar)} 59 60def _list_jar_classes(jar): 61 with ZipFile(jar, 'r') as zip: 62 files = zip.namelist() 63 assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \ 64 'expected an intermediate zip of .class files' 65 class_len = len('.class') 66 return [f.replace('/', '.')[:-class_len] for f in files 67 if f.endswith('.class') and not f.endswith('/package-info.class')] 68 69 70def _list_hiddenapi_classes(txt_file): 71 out = set() 72 with open(txt_file, 'r') as f: 73 for line in f: 74 if not line.strip(): 75 continue 76 assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}' 77 clazz = line.replace('/', '.').split(';')[0][1:] 78 out.add(_get_toplevel_class(clazz)) 79 return out 80 81 82def _get_toplevel_class(clazz): 83 """Return the name of the toplevel (not an inner class) enclosing class of the given class.""" 84 if '$' not in clazz: 85 return clazz 86 return clazz.split('$')[0] 87 88 89def _get_excludes(path): 90 out = [] 91 with open(path, 'r') as f: 92 for line in f: 93 stripped = line.strip() 94 if not stripped or stripped.startswith('#'): 95 continue 96 out.append(re.compile(stripped)) 97 return out 98 99 100def make_jarjar_rules(args): 101 excluded_classes = set() 102 for apistubs_file in args.apistubs: 103 excluded_classes.update(_list_toplevel_jar_classes(apistubs_file)) 104 105 unsupportedapi_files = (args.unsupportedapi and args.unsupportedapi.split(':')) or [] 106 for unsupportedapi_file in unsupportedapi_files: 107 if unsupportedapi_file: 108 excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file)) 109 110 exclude_regexes = [] 111 for exclude_file in args.excludes: 112 exclude_regexes.extend(_get_excludes(exclude_file)) 113 114 with open(args.output, 'w') as outfile: 115 for jar in args.jars: 116 jar_classes = _list_jar_classes(jar) 117 jar_classes.sort() 118 for clazz in jar_classes: 119 if (not clazz.startswith(args.prefix + '.') and 120 _get_toplevel_class(clazz) not in excluded_classes and 121 not any(r.fullmatch(clazz) for r in exclude_regexes)): 122 outfile.write(f'rule {clazz} {args.prefix}.@0\n') 123 # Also include jarjar rules for unit tests of the class if it's not explicitly 124 # excluded, so the package matches 125 if not any(r.fullmatch(clazz + 'Test') for r in exclude_regexes): 126 outfile.write(f'rule {clazz}Test {args.prefix}.@0\n') 127 outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n') 128 129 130def _main(): 131 # Pass in None to use argv 132 args = parse_arguments(None) 133 make_jarjar_rules(args) 134 135 136if __name__ == '__main__': 137 _main() 138