Remi NGUYEN VAN | 7b92ff2 | 2022-04-20 15:59:16 +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( |
| 31 | '--jars', nargs='+', |
| 32 | help='Path to pre-jarjar JAR. Can be followed by multiple space-separated paths.') |
| 33 | parser.add_argument( |
| 34 | '--prefix', required=True, help='Package prefix to use for jarjared classes.') |
| 35 | parser.add_argument( |
| 36 | '--output', required=True, help='Path to output jarjar rules file.') |
| 37 | parser.add_argument( |
| 38 | '--apistubs', nargs='*', default=[], |
| 39 | help='Path to API stubs jar. Classes that are API will not be jarjared. Can be followed by ' |
| 40 | 'multiple space-separated paths.') |
| 41 | parser.add_argument( |
| 42 | '--unsupportedapi', nargs='*', default=[], |
| 43 | help='Path to UnsupportedAppUsage hidden API .txt lists. ' |
| 44 | 'Classes that have UnsupportedAppUsage API will not be jarjared. Can be followed by ' |
| 45 | 'multiple space-separated paths.') |
| 46 | parser.add_argument( |
| 47 | '--excludes', nargs='*', default=[], |
| 48 | help='Path to files listing classes that should not be jarjared. Can be followed by ' |
| 49 | 'multiple space-separated paths. ' |
| 50 | 'Each file should contain one full-match regex per line. Empty lines or lines ' |
| 51 | 'starting with "#" are ignored.') |
| 52 | parser.add_argument( |
| 53 | '--dexdump', default='dexdump', help='Path to dexdump binary.') |
| 54 | return parser.parse_args(argv) |
| 55 | |
| 56 | |
| 57 | class DumpHandler(ContentHandler): |
| 58 | def __init__(self): |
| 59 | super().__init__() |
| 60 | self._current_package = None |
| 61 | self.classes = [] |
| 62 | |
| 63 | def startElement(self, name, attrs): |
| 64 | if name == 'package': |
| 65 | attr_name = attrs.getValue('name') |
| 66 | assert attr_name != '', '<package> element missing name' |
| 67 | assert self._current_package is None, f'Found nested package tags for {attr_name}' |
| 68 | self._current_package = attr_name |
| 69 | elif name == 'class': |
| 70 | attr_name = attrs.getValue('name') |
| 71 | assert attr_name != '', '<class> element missing name' |
| 72 | self.classes.append(self._current_package + '.' + attr_name) |
| 73 | |
| 74 | def endElement(self, name): |
| 75 | if name == 'package': |
| 76 | self._current_package = None |
| 77 | |
| 78 | |
| 79 | def _list_toplevel_dex_classes(jar, dexdump): |
| 80 | """List all classes in a dexed .jar file that are not inner classes.""" |
| 81 | # Empty jars do net get a classes.dex: return an empty set for them |
| 82 | with ZipFile(jar, 'r') as zip_file: |
| 83 | if not zip_file.namelist(): |
| 84 | return set() |
| 85 | cmd = [dexdump, '-l', 'xml', '-e', jar] |
| 86 | dump = subprocess.run(cmd, check=True, text=True, stdout=subprocess.PIPE) |
| 87 | handler = DumpHandler() |
| 88 | xml_parser = sax.make_parser() |
| 89 | xml_parser.setContentHandler(handler) |
| 90 | xml_parser.parse(io.StringIO(dump.stdout)) |
| 91 | return set([_get_toplevel_class(c) for c in handler.classes]) |
| 92 | |
| 93 | |
| 94 | def _list_jar_classes(jar): |
| 95 | with ZipFile(jar, 'r') as zip: |
| 96 | files = zip.namelist() |
| 97 | assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \ |
| 98 | 'expected an intermediate zip of .class files' |
| 99 | class_len = len('.class') |
| 100 | return [f.replace('/', '.')[:-class_len] for f in files |
| 101 | if f.endswith('.class') and not f.endswith('/package-info.class')] |
| 102 | |
| 103 | |
| 104 | def _list_hiddenapi_classes(txt_file): |
| 105 | out = set() |
| 106 | with open(txt_file, 'r') as f: |
| 107 | for line in f: |
| 108 | if not line.strip(): |
| 109 | continue |
| 110 | assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}' |
| 111 | clazz = line.replace('/', '.').split(';')[0][1:] |
| 112 | out.add(_get_toplevel_class(clazz)) |
| 113 | return out |
| 114 | |
| 115 | |
| 116 | def _get_toplevel_class(clazz): |
| 117 | """Return the name of the toplevel (not an inner class) enclosing class of the given class.""" |
| 118 | if '$' not in clazz: |
| 119 | return clazz |
| 120 | return clazz.split('$')[0] |
| 121 | |
| 122 | |
| 123 | def _get_excludes(path): |
| 124 | out = [] |
| 125 | with open(path, 'r') as f: |
| 126 | for line in f: |
| 127 | stripped = line.strip() |
| 128 | if not stripped or stripped.startswith('#'): |
| 129 | continue |
| 130 | out.append(re.compile(stripped)) |
| 131 | return out |
| 132 | |
| 133 | |
| 134 | def make_jarjar_rules(args): |
| 135 | excluded_classes = set() |
| 136 | for apistubs_file in args.apistubs: |
| 137 | excluded_classes.update(_list_toplevel_dex_classes(apistubs_file, args.dexdump)) |
| 138 | |
| 139 | for unsupportedapi_file in args.unsupportedapi: |
| 140 | excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file)) |
| 141 | |
| 142 | exclude_regexes = [] |
| 143 | for exclude_file in args.excludes: |
| 144 | exclude_regexes.extend(_get_excludes(exclude_file)) |
| 145 | |
| 146 | with open(args.output, 'w') as outfile: |
| 147 | for jar in args.jars: |
| 148 | jar_classes = _list_jar_classes(jar) |
| 149 | jar_classes.sort() |
| 150 | for clazz in jar_classes: |
| 151 | if (_get_toplevel_class(clazz) not in excluded_classes and |
| 152 | not any(r.fullmatch(clazz) for r in exclude_regexes)): |
| 153 | outfile.write(f'rule {clazz} {args.prefix}.@0\n') |
| 154 | # Also include jarjar rules for unit tests of the class, so the package matches |
| 155 | outfile.write(f'rule {clazz}Test {args.prefix}.@0\n') |
| 156 | outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n') |
| 157 | |
| 158 | |
| 159 | def _main(): |
| 160 | # Pass in None to use argv |
| 161 | args = parse_arguments(None) |
| 162 | make_jarjar_rules(args) |
| 163 | |
| 164 | |
| 165 | if __name__ == '__main__': |
| 166 | _main() |