| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 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 | 
 | 17 | that are API, unsupported API or otherwise excluded.""" | 
 | 18 |  | 
 | 19 | import argparse | 
 | 20 | import io | 
 | 21 | import re | 
 | 22 | import subprocess | 
 | 23 | from xml import sax | 
 | 24 | from xml.sax.handler import ContentHandler | 
 | 25 | from zipfile import ZipFile | 
 | 26 |  | 
 | 27 |  | 
 | 28 | def parse_arguments(argv): | 
 | 29 |     parser = argparse.ArgumentParser() | 
 | 30 |     parser.add_argument( | 
| Remi NGUYEN VAN | 0bd90f1 | 2022-08-10 20:15:46 +0900 | [diff] [blame] | 31 |         'jars', nargs='+', | 
 | 32 |         help='Path to pre-jarjar JAR. Multiple jars can be specified.') | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 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( | 
| Remi NGUYEN VAN | 0bd90f1 | 2022-08-10 20:15:46 +0900 | [diff] [blame] | 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.') | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 43 |     parser.add_argument( | 
| Remi NGUYEN VAN | 0bd90f1 | 2022-08-10 20:15:46 +0900 | [diff] [blame] | 44 |         '--unsupportedapi', | 
 | 45 |         help='Column(:)-separated paths to UnsupportedAppUsage hidden API .txt lists. ' | 
 | 46 |              'Classes that have UnsupportedAppUsage API will not be jarjared.') | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 47 |     parser.add_argument( | 
| Remi NGUYEN VAN | 0bd90f1 | 2022-08-10 20:15:46 +0900 | [diff] [blame] | 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.' | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 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 |  | 
 | 56 | def _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 |  | 
 | 60 | def _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 |  | 
 | 70 | def _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 |  | 
 | 82 | def _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 |  | 
 | 89 | def _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 |  | 
 | 100 | def 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 |  | 
| Remi NGUYEN VAN | 0bd90f1 | 2022-08-10 20:15:46 +0900 | [diff] [blame] | 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)) | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 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: | 
| Remi NGUYEN VAN | 3ba00cb | 2022-07-21 18:57:41 +0900 | [diff] [blame] | 119 |                 if (not clazz.startswith(args.prefix + '.') and | 
 | 120 |                         _get_toplevel_class(clazz) not in excluded_classes and | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 121 |                         not any(r.fullmatch(clazz) for r in exclude_regexes)): | 
 | 122 |                     outfile.write(f'rule {clazz} {args.prefix}.@0\n') | 
| Chidera Olibie | 6942957 | 2023-03-20 23:05:40 +0000 | [diff] [blame] | 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') | 
| Remi NGUYEN VAN | 11f162b | 2022-05-24 16:47:33 +0900 | [diff] [blame] | 128 |  | 
 | 129 |  | 
 | 130 | def _main(): | 
 | 131 |     # Pass in None to use argv | 
 | 132 |     args = parse_arguments(None) | 
 | 133 |     make_jarjar_rules(args) | 
 | 134 |  | 
 | 135 |  | 
 | 136 | if __name__ == '__main__': | 
 | 137 |     _main() |