blob: 4ef4399ca1a5fca47e9de7b3524be0b4a405f921 [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 Trafimovichbb7513d2021-03-30 17:15:16 +010077def enforce_uses_libraries(manifest, required, optional, relax, is_apk, path):
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000078 """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:
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +010089 manifest_required, manifest_optional, tags = extract_uses_libs_apk(manifest)
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000090 else:
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +010091 manifest_required, manifest_optional, tags = extract_uses_libs_xml(manifest)
Colin Cross72119102019-05-20 13:14:18 -070092
Ulya Trafimovich1b513452021-07-20 14:27:32 +010093 # Trim namespace component. Normally Soong does that automatically when it
94 # handles module names specified in Android.bp properties. However not all
95 # <uses-library> entries in the manifest correspond to real modules: some of
96 # the optional libraries may be missing at build time. Therefor this script
97 # accepts raw module names as spelled in Android.bp/Amdroid.mk and trims the
98 # optional namespace part manually.
99 required = trim_namespace_parts(required)
100 optional = trim_namespace_parts(optional)
101
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100102 if manifest_required == required and manifest_optional == optional:
103 return None
Colin Cross72119102019-05-20 13:14:18 -0700104
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100105 errmsg = ''.join([
106 'mismatch in the <uses-library> tags between the build system and the '
107 'manifest:\n',
108 '\t- required libraries in build system: [%s]\n' % ', '.join(required),
109 '\t vs. in the manifest: [%s]\n' % ', '.join(manifest_required),
110 '\t- optional libraries in build system: [%s]\n' % ', '.join(optional),
111 '\t vs. in the manifest: [%s]\n' % ', '.join(manifest_optional),
112 '\t- tags in the manifest (%s):\n' % path,
113 '\t\t%s\n' % '\t\t'.join(tags),
114 'note: the following options are available:\n',
115 '\t- to temporarily disable the check on command line, rebuild with ',
116 'RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" ',
117 'and disable AOT-compilation in dexpreopt)\n',
118 '\t- to temporarily disable the check for the whole product, set ',
119 'PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles\n',
120 '\t- to fix the check, make build system properties coherent with the '
121 'manifest\n',
122 '\t- see build/make/Changes.md for details\n'])
Colin Cross72119102019-05-20 13:14:18 -0700123
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100124 if not relax:
125 raise ManifestMismatchError(errmsg)
Colin Cross72119102019-05-20 13:14:18 -0700126
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100127 return errmsg
Colin Cross72119102019-05-20 13:14:18 -0700128
Colin Cross72119102019-05-20 13:14:18 -0700129
Ulya Trafimovich1b513452021-07-20 14:27:32 +0100130MODULE_NAMESPACE = re.compile("^//[^:]+:")
131
132def trim_namespace_parts(modules):
133 """Trim the namespace part of each module, if present. Leave only the name."""
134
135 trimmed = []
136 for module in modules:
137 trimmed.append(MODULE_NAMESPACE.sub('', module))
138 return trimmed
139
140
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000141def extract_uses_libs_apk(badging):
142 """Extract <uses-library> tags from the manifest of an APK."""
143
144 pattern = re.compile("^uses-library(-not-required)?:'(.*)'$", re.MULTILINE)
145
146 required = []
147 optional = []
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100148 lines = []
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000149 for match in re.finditer(pattern, badging):
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100150 lines.append(match.group(0))
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000151 libname = match.group(2)
152 if match.group(1) == None:
153 required.append(libname)
154 else:
155 optional.append(libname)
156
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100157 required = first_unique_elements(required)
158 optional = first_unique_elements(optional)
159 tags = first_unique_elements(lines)
160 return required, optional, tags
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000161
162
163def extract_uses_libs_xml(xml):
164 """Extract <uses-library> tags from the manifest."""
165
166 manifest = parse_manifest(xml)
167 elems = get_children_with_tag(manifest, 'application')
168 application = elems[0] if len(elems) == 1 else None
169 if len(elems) > 1:
170 raise RuntimeError('found multiple <application> tags')
171 elif not elems:
172 if uses_libraries or optional_uses_libraries:
173 raise ManifestMismatchError('no <application> tag found')
174 return
Colin Cross72119102019-05-20 13:14:18 -0700175
176 libs = get_children_with_tag(application, 'uses-library')
177
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000178 required = [uses_library_name(x) for x in libs if uses_library_required(x)]
179 optional = [uses_library_name(x) for x in libs if not uses_library_required(x)]
Colin Cross72119102019-05-20 13:14:18 -0700180
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100181 # render <uses-library> tags as XML for a pretty error message
182 tags = []
183 for lib in libs:
184 tags.append(lib.toprettyxml())
185
186 required = first_unique_elements(required)
187 optional = first_unique_elements(optional)
188 tags = first_unique_elements(tags)
189 return required, optional, tags
Colin Cross72119102019-05-20 13:14:18 -0700190
191
192def first_unique_elements(l):
193 result = []
194 [result.append(x) for x in l if x not in result]
195 return result
196
197
198def uses_library_name(lib):
199 """Extract the name attribute of a uses-library tag.
200
201 Args:
202 lib: a <uses-library> tag.
203 """
204 name = lib.getAttributeNodeNS(android_ns, 'name')
205 return name.value if name is not None else ""
206
207
208def uses_library_required(lib):
209 """Extract the required attribute of a uses-library tag.
210
211 Args:
212 lib: a <uses-library> tag.
213 """
214 required = lib.getAttributeNodeNS(android_ns, 'required')
215 return (required.value == 'true') if required is not None else True
216
217
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000218def extract_target_sdk_version(manifest, is_apk = False):
Colin Cross72119102019-05-20 13:14:18 -0700219 """Returns the targetSdkVersion from the manifest.
220
221 Args:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000222 manifest: manifest (either parsed XML or aapt dump of APK)
223 is_apk: if the manifest comes from an APK or an XML file
Colin Cross72119102019-05-20 13:14:18 -0700224 """
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000225 if is_apk:
226 return extract_target_sdk_version_apk(manifest)
227 else:
228 return extract_target_sdk_version_xml(manifest)
Colin Cross72119102019-05-20 13:14:18 -0700229
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000230
231def extract_target_sdk_version_apk(badging):
232 """Extract targetSdkVersion tags from the manifest of an APK."""
233
234 pattern = re.compile("^targetSdkVersion?:'(.*)'$", re.MULTILINE)
235
236 for match in re.finditer(pattern, badging):
237 return match.group(1)
238
239 raise RuntimeError('cannot find targetSdkVersion in the manifest')
240
241
242def extract_target_sdk_version_xml(xml):
243 """Extract targetSdkVersion tags from the manifest."""
244
245 manifest = parse_manifest(xml)
Colin Cross72119102019-05-20 13:14:18 -0700246
247 # Get or insert the uses-sdk element
248 uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
249 if len(uses_sdk) > 1:
250 raise RuntimeError('found multiple uses-sdk elements')
251 elif len(uses_sdk) == 0:
252 raise RuntimeError('missing uses-sdk element')
253
254 uses_sdk = uses_sdk[0]
255
256 min_attr = uses_sdk.getAttributeNodeNS(android_ns, 'minSdkVersion')
257 if min_attr is None:
258 raise RuntimeError('minSdkVersion is not specified')
259
260 target_attr = uses_sdk.getAttributeNodeNS(android_ns, 'targetSdkVersion')
261 if target_attr is None:
262 target_attr = min_attr
263
264 return target_attr.value
265
266
Ulya Trafimovich3c902e72021-03-04 18:06:27 +0000267def load_dexpreopt_configs(configs):
268 """Load dexpreopt.config files and map module names to library names."""
269 module_to_libname = {}
270
271 if configs is None:
272 configs = []
273
274 for config in configs:
275 with open(config, 'r') as f:
276 contents = json.load(f)
277 module_to_libname[contents['Name']] = contents['ProvidesUsesLibrary']
278
279 return module_to_libname
280
281
282def translate_libnames(modules, module_to_libname):
283 """Translate module names into library names using the mapping."""
284 if modules is None:
285 modules = []
286
287 libnames = []
288 for name in modules:
289 if name in module_to_libname:
290 name = module_to_libname[name]
291 libnames.append(name)
292
293 return libnames
294
295
Colin Cross72119102019-05-20 13:14:18 -0700296def main():
297 """Program entry point."""
298 try:
299 args = parse_args()
300
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000301 # The input can be either an XML manifest or an APK, they are parsed and
302 # processed in different ways.
303 is_apk = args.input.endswith('.apk')
304 if is_apk:
305 aapt = args.aapt if args.aapt != None else "aapt"
306 manifest = subprocess.check_output([aapt, "dump", "badging", args.input])
307 else:
308 manifest = minidom.parse(args.input)
Colin Cross72119102019-05-20 13:14:18 -0700309
310 if args.enforce_uses_libraries:
Ulya Trafimovich3c902e72021-03-04 18:06:27 +0000311 # Load dexpreopt.config files and build a mapping from module names to
312 # library names. This is necessary because build system addresses
313 # libraries by their module name (`uses_libs`, `optional_uses_libs`,
314 # `LOCAL_USES_LIBRARIES`, `LOCAL_OPTIONAL_LIBRARY_NAMES` all contain
315 # module names), while the manifest addresses libraries by their name.
316 mod_to_lib = load_dexpreopt_configs(args.dexpreopt_configs)
317 required = translate_libnames(args.uses_libraries, mod_to_lib)
318 optional = translate_libnames(args.optional_uses_libraries, mod_to_lib)
319
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000320 # Check if the <uses-library> lists in the build system agree with those
321 # in the manifest. Raise an exception on mismatch, unless the script was
322 # passed a special parameter to suppress exceptions.
Ulya Trafimovich3c902e72021-03-04 18:06:27 +0000323 errmsg = enforce_uses_libraries(manifest, required, optional,
Ulya Trafimovichbb7513d2021-03-30 17:15:16 +0100324 args.enforce_uses_libraries_relax, is_apk, args.input)
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000325
326 # Create a status file that is empty on success, or contains an error
327 # message on failure. When exceptions are suppressed, dexpreopt command
328 # command will check file size to determine if the check has failed.
329 if args.enforce_uses_libraries_status:
330 with open(args.enforce_uses_libraries_status, 'w') as f:
331 if not errmsg == None:
332 f.write("%s\n" % errmsg)
Colin Cross72119102019-05-20 13:14:18 -0700333
334 if args.extract_target_sdk_version:
Ulya Trafimovich9f12df92021-03-29 14:06:58 +0100335 try:
336 print(extract_target_sdk_version(manifest, is_apk))
337 except:
338 # Failed; don't crash, return "any" SDK version. This will result in
339 # dexpreopt not adding any compatibility libraries.
340 print(10000)
Colin Cross72119102019-05-20 13:14:18 -0700341
342 if args.output:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000343 # XML output is supposed to be written only when this script is invoked
344 # with XML input manifest, not with an APK.
345 if is_apk:
346 raise RuntimeError('cannot save APK manifest as XML')
347
Colin Cross72119102019-05-20 13:14:18 -0700348 with open(args.output, 'wb') as f:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000349 write_xml(f, manifest)
Colin Cross72119102019-05-20 13:14:18 -0700350
351 # pylint: disable=broad-except
352 except Exception as err:
353 print('error: ' + str(err), file=sys.stderr)
354 sys.exit(-1)
355
356if __name__ == '__main__':
357 main()