blob: 1343f356342e629c8d76f6631fd4ace3b297d882 [file] [log] [blame]
Colin Cross72119102019-05-20 13:14:18 -07001#!/usr/bin/env python
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17"""A tool for checking that a manifest agrees with the build system."""
18
19from __future__ import print_function
20
21import argparse
Ulya Trafimovich3c902e72021-03-04 18:06:27 +000022import json
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000023import re
24import subprocess
Colin Cross72119102019-05-20 13:14:18 -070025import sys
26from xml.dom import minidom
27
28
29from manifest import android_ns
30from manifest import get_children_with_tag
31from manifest import parse_manifest
32from manifest import write_xml
33
34
35class ManifestMismatchError(Exception):
36 pass
37
38
39def parse_args():
40 """Parse commandline arguments."""
41
42 parser = argparse.ArgumentParser()
43 parser.add_argument('--uses-library', dest='uses_libraries',
44 action='append',
45 help='specify uses-library entries known to the build system')
46 parser.add_argument('--optional-uses-library',
47 dest='optional_uses_libraries',
48 action='append',
49 help='specify uses-library entries known to the build system with required:false')
50 parser.add_argument('--enforce-uses-libraries',
51 dest='enforce_uses_libraries',
52 action='store_true',
53 help='check the uses-library entries known to the build system against the manifest')
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +000054 parser.add_argument('--enforce-uses-libraries-relax',
55 dest='enforce_uses_libraries_relax',
56 action='store_true',
57 help='do not fail immediately, just save the error message to file')
58 parser.add_argument('--enforce-uses-libraries-status',
59 dest='enforce_uses_libraries_status',
60 help='output file to store check status (error message)')
Colin Cross72119102019-05-20 13:14:18 -070061 parser.add_argument('--extract-target-sdk-version',
62 dest='extract_target_sdk_version',
63 action='store_true',
64 help='print the targetSdkVersion from the manifest')
Ulya Trafimovich3c902e72021-03-04 18:06:27 +000065 parser.add_argument('--dexpreopt-config',
66 dest='dexpreopt_configs',
67 action='append',
68 help='a paths to a dexpreopt.config of some library')
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000069 parser.add_argument('--aapt',
70 dest='aapt',
71 help='path to aapt executable')
Colin Cross72119102019-05-20 13:14:18 -070072 parser.add_argument('--output', '-o', dest='output', help='output AndroidManifest.xml file')
73 parser.add_argument('input', help='input AndroidManifest.xml file')
74 return parser.parse_args()
75
76
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000077def enforce_uses_libraries(manifest, required, optional, relax, is_apk = False):
78 """Verify that the <uses-library> tags in the manifest match those provided
79 by the build system.
Colin Cross72119102019-05-20 13:14:18 -070080
81 Args:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000082 manifest: manifest (either parsed XML or aapt dump of APK)
83 required: required libs known to the build system
84 optional: optional libs known to the build system
85 relax: if true, suppress error on mismatch and just write it to file
86 is_apk: if the manifest comes from an APK or an XML file
Colin Cross72119102019-05-20 13:14:18 -070087 """
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000088 if is_apk:
89 manifest_required, manifest_optional = extract_uses_libs_apk(manifest)
90 else:
91 manifest_required, manifest_optional = extract_uses_libs_xml(manifest)
Colin Cross72119102019-05-20 13:14:18 -070092
Colin Cross72119102019-05-20 13:14:18 -070093 err = []
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000094 if manifest_required != required:
Colin Cross72119102019-05-20 13:14:18 -070095 err.append('Expected required <uses-library> tags "%s", got "%s"' %
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000096 (', '.join(required), ', '.join(manifest_required)))
Colin Cross72119102019-05-20 13:14:18 -070097
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000098 if manifest_optional != optional:
Colin Cross72119102019-05-20 13:14:18 -070099 err.append('Expected optional <uses-library> tags "%s", got "%s"' %
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000100 (', '.join(optional), ', '.join(manifest_optional)))
Colin Cross72119102019-05-20 13:14:18 -0700101
102 if err:
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000103 errmsg = '\n'.join(err)
104 if not relax:
105 raise ManifestMismatchError(errmsg)
106 return errmsg
Colin Cross72119102019-05-20 13:14:18 -0700107
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000108 return None
Colin Cross72119102019-05-20 13:14:18 -0700109
Colin Cross72119102019-05-20 13:14:18 -0700110
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000111def extract_uses_libs_apk(badging):
112 """Extract <uses-library> tags from the manifest of an APK."""
113
114 pattern = re.compile("^uses-library(-not-required)?:'(.*)'$", re.MULTILINE)
115
116 required = []
117 optional = []
118 for match in re.finditer(pattern, badging):
119 libname = match.group(2)
120 if match.group(1) == None:
121 required.append(libname)
122 else:
123 optional.append(libname)
124
125 return first_unique_elements(required), first_unique_elements(optional)
126
127
128def extract_uses_libs_xml(xml):
129 """Extract <uses-library> tags from the manifest."""
130
131 manifest = parse_manifest(xml)
132 elems = get_children_with_tag(manifest, 'application')
133 application = elems[0] if len(elems) == 1 else None
134 if len(elems) > 1:
135 raise RuntimeError('found multiple <application> tags')
136 elif not elems:
137 if uses_libraries or optional_uses_libraries:
138 raise ManifestMismatchError('no <application> tag found')
139 return
Colin Cross72119102019-05-20 13:14:18 -0700140
141 libs = get_children_with_tag(application, 'uses-library')
142
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000143 required = [uses_library_name(x) for x in libs if uses_library_required(x)]
144 optional = [uses_library_name(x) for x in libs if not uses_library_required(x)]
Colin Cross72119102019-05-20 13:14:18 -0700145
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000146 return first_unique_elements(required), first_unique_elements(optional)
Colin Cross72119102019-05-20 13:14:18 -0700147
148
149def first_unique_elements(l):
150 result = []
151 [result.append(x) for x in l if x not in result]
152 return result
153
154
155def uses_library_name(lib):
156 """Extract the name attribute of a uses-library tag.
157
158 Args:
159 lib: a <uses-library> tag.
160 """
161 name = lib.getAttributeNodeNS(android_ns, 'name')
162 return name.value if name is not None else ""
163
164
165def uses_library_required(lib):
166 """Extract the required attribute of a uses-library tag.
167
168 Args:
169 lib: a <uses-library> tag.
170 """
171 required = lib.getAttributeNodeNS(android_ns, 'required')
172 return (required.value == 'true') if required is not None else True
173
174
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000175def extract_target_sdk_version(manifest, is_apk = False):
Colin Cross72119102019-05-20 13:14:18 -0700176 """Returns the targetSdkVersion from the manifest.
177
178 Args:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000179 manifest: manifest (either parsed XML or aapt dump of APK)
180 is_apk: if the manifest comes from an APK or an XML file
Colin Cross72119102019-05-20 13:14:18 -0700181 """
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000182 if is_apk:
183 return extract_target_sdk_version_apk(manifest)
184 else:
185 return extract_target_sdk_version_xml(manifest)
Colin Cross72119102019-05-20 13:14:18 -0700186
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000187
188def extract_target_sdk_version_apk(badging):
189 """Extract targetSdkVersion tags from the manifest of an APK."""
190
191 pattern = re.compile("^targetSdkVersion?:'(.*)'$", re.MULTILINE)
192
193 for match in re.finditer(pattern, badging):
194 return match.group(1)
195
196 raise RuntimeError('cannot find targetSdkVersion in the manifest')
197
198
199def extract_target_sdk_version_xml(xml):
200 """Extract targetSdkVersion tags from the manifest."""
201
202 manifest = parse_manifest(xml)
Colin Cross72119102019-05-20 13:14:18 -0700203
204 # Get or insert the uses-sdk element
205 uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
206 if len(uses_sdk) > 1:
207 raise RuntimeError('found multiple uses-sdk elements')
208 elif len(uses_sdk) == 0:
209 raise RuntimeError('missing uses-sdk element')
210
211 uses_sdk = uses_sdk[0]
212
213 min_attr = uses_sdk.getAttributeNodeNS(android_ns, 'minSdkVersion')
214 if min_attr is None:
215 raise RuntimeError('minSdkVersion is not specified')
216
217 target_attr = uses_sdk.getAttributeNodeNS(android_ns, 'targetSdkVersion')
218 if target_attr is None:
219 target_attr = min_attr
220
221 return target_attr.value
222
223
Ulya Trafimovich3c902e72021-03-04 18:06:27 +0000224def load_dexpreopt_configs(configs):
225 """Load dexpreopt.config files and map module names to library names."""
226 module_to_libname = {}
227
228 if configs is None:
229 configs = []
230
231 for config in configs:
232 with open(config, 'r') as f:
233 contents = json.load(f)
234 module_to_libname[contents['Name']] = contents['ProvidesUsesLibrary']
235
236 return module_to_libname
237
238
239def translate_libnames(modules, module_to_libname):
240 """Translate module names into library names using the mapping."""
241 if modules is None:
242 modules = []
243
244 libnames = []
245 for name in modules:
246 if name in module_to_libname:
247 name = module_to_libname[name]
248 libnames.append(name)
249
250 return libnames
251
252
Colin Cross72119102019-05-20 13:14:18 -0700253def main():
254 """Program entry point."""
255 try:
256 args = parse_args()
257
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000258 # The input can be either an XML manifest or an APK, they are parsed and
259 # processed in different ways.
260 is_apk = args.input.endswith('.apk')
261 if is_apk:
262 aapt = args.aapt if args.aapt != None else "aapt"
263 manifest = subprocess.check_output([aapt, "dump", "badging", args.input])
264 else:
265 manifest = minidom.parse(args.input)
Colin Cross72119102019-05-20 13:14:18 -0700266
267 if args.enforce_uses_libraries:
Ulya Trafimovich3c902e72021-03-04 18:06:27 +0000268 # Load dexpreopt.config files and build a mapping from module names to
269 # library names. This is necessary because build system addresses
270 # libraries by their module name (`uses_libs`, `optional_uses_libs`,
271 # `LOCAL_USES_LIBRARIES`, `LOCAL_OPTIONAL_LIBRARY_NAMES` all contain
272 # module names), while the manifest addresses libraries by their name.
273 mod_to_lib = load_dexpreopt_configs(args.dexpreopt_configs)
274 required = translate_libnames(args.uses_libraries, mod_to_lib)
275 optional = translate_libnames(args.optional_uses_libraries, mod_to_lib)
276
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000277 # Check if the <uses-library> lists in the build system agree with those
278 # in the manifest. Raise an exception on mismatch, unless the script was
279 # passed a special parameter to suppress exceptions.
Ulya Trafimovich3c902e72021-03-04 18:06:27 +0000280 errmsg = enforce_uses_libraries(manifest, required, optional,
281 args.enforce_uses_libraries_relax, is_apk)
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000282
283 # Create a status file that is empty on success, or contains an error
284 # message on failure. When exceptions are suppressed, dexpreopt command
285 # command will check file size to determine if the check has failed.
286 if args.enforce_uses_libraries_status:
287 with open(args.enforce_uses_libraries_status, 'w') as f:
288 if not errmsg == None:
289 f.write("%s\n" % errmsg)
Colin Cross72119102019-05-20 13:14:18 -0700290
291 if args.extract_target_sdk_version:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000292 print(extract_target_sdk_version(manifest, is_apk))
Colin Cross72119102019-05-20 13:14:18 -0700293
294 if args.output:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000295 # XML output is supposed to be written only when this script is invoked
296 # with XML input manifest, not with an APK.
297 if is_apk:
298 raise RuntimeError('cannot save APK manifest as XML')
299
Colin Cross72119102019-05-20 13:14:18 -0700300 with open(args.output, 'wb') as f:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000301 write_xml(f, manifest)
Colin Cross72119102019-05-20 13:14:18 -0700302
303 # pylint: disable=broad-except
304 except Exception as err:
305 print('error: ' + str(err), file=sys.stderr)
306 sys.exit(-1)
307
308if __name__ == '__main__':
309 main()