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