blob: 6fdf3f4261945dc59344fee39efc7754e7eecd3e [file] [log] [blame]
Remi NGUYEN VAN7b92ff22022-04-20 15:59:16 +09001#
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. 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
57class 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
79def _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
94def _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
104def _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
116def _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
123def _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
134def 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
159def _main():
160 # Pass in None to use argv
161 args = parse_arguments(None)
162 make_jarjar_rules(args)
163
164
165if __name__ == '__main__':
166 _main()