• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/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
22import json
23import re
24import subprocess
25import sys
26from xml.dom import minidom
27
28from manifest import android_ns
29from manifest import get_children_with_tag
30from manifest import parse_manifest
31from manifest import write_xml
32
33
34class ManifestMismatchError(Exception):
35    pass
36
37
38def parse_args():
39    """Parse commandline arguments."""
40
41    parser = argparse.ArgumentParser()
42    parser.add_argument(
43        '--uses-library',
44        dest='uses_libraries',
45        action='append',
46        help='specify uses-library entries known to the build system')
47    parser.add_argument(
48        '--optional-uses-library',
49        dest='optional_uses_libraries',
50        action='append',
51        help='specify uses-library entries known to the build system with '
52        'required:false'
53    )
54    parser.add_argument(
55        '--enforce-uses-libraries',
56        dest='enforce_uses_libraries',
57        action='store_true',
58        help='check the uses-library entries known to the build system against '
59        'the manifest'
60    )
61    parser.add_argument(
62        '--enforce-uses-libraries-relax',
63        dest='enforce_uses_libraries_relax',
64        action='store_true',
65        help='do not fail immediately, just save the error message to file')
66    parser.add_argument(
67        '--enforce-uses-libraries-status',
68        dest='enforce_uses_libraries_status',
69        help='output file to store check status (error message)')
70    parser.add_argument(
71        '--extract-target-sdk-version',
72        dest='extract_target_sdk_version',
73        action='store_true',
74        help='print the targetSdkVersion from the manifest')
75    parser.add_argument(
76        '--dexpreopt-config',
77        dest='dexpreopt_configs',
78        action='append',
79        help='a paths to a dexpreopt.config of some library')
80    parser.add_argument('--aapt', dest='aapt', help='path to aapt executable')
81    parser.add_argument(
82        '--output', '-o', dest='output', help='output AndroidManifest.xml file')
83    parser.add_argument('input', help='input AndroidManifest.xml file')
84    return parser.parse_args()
85
86
87C_RED = "\033[1;31m"
88C_GREEN = "\033[1;32m"
89C_BLUE = "\033[1;34m"
90C_OFF = "\033[0m"
91C_BOLD = "\033[1m"
92
93
94def enforce_uses_libraries(manifest, required, optional, relax, is_apk, path):
95    """Verify that the <uses-library> tags in the manifest match those provided
96
97  by the build system.
98
99  Args:
100    manifest: manifest (either parsed XML or aapt dump of APK)
101    required: required libs known to the build system
102    optional: optional libs known to the build system
103    relax:    if true, suppress error on mismatch and just write it to file
104    is_apk:   if the manifest comes from an APK or an XML file
105    """
106    if is_apk:
107        manifest_required, manifest_optional, tags = extract_uses_libs_apk(
108            manifest)
109    else:
110        manifest_required, manifest_optional, tags = extract_uses_libs_xml(
111            manifest)
112
113    # Trim namespace component. Normally Soong does that automatically when it
114    # handles module names specified in Android.bp properties. However not all
115    # <uses-library> entries in the manifest correspond to real modules: some of
116    # the optional libraries may be missing at build time. Therefor this script
117    # accepts raw module names as spelled in Android.bp/Amdroid.mk and trims the
118    # optional namespace part manually.
119    required = trim_namespace_parts(required)
120    optional = trim_namespace_parts(optional)
121
122    if manifest_required == required and manifest_optional == optional:
123        return None
124
125    #pylint: disable=line-too-long
126    errmsg = ''.join([
127        'mismatch in the <uses-library> tags between the build system and the '
128        'manifest:\n',
129        '\t- required libraries in build system: %s[%s]%s\n' % (C_RED, ', '.join(required), C_OFF),
130        '\t                 vs. in the manifest: %s[%s]%s\n' % (C_RED, ', '.join(manifest_required), C_OFF),
131        '\t- optional libraries in build system: %s[%s]%s\n' % (C_RED, ', '.join(optional), C_OFF),
132        '\t                 vs. in the manifest: %s[%s]%s\n' % (C_RED, ', '.join(manifest_optional), C_OFF),
133        '\t- tags in the manifest (%s):\n' % path,
134        '\t\t%s\n' % '\t\t'.join(tags),
135        '%snote:%s the following options are available:\n' % (C_BLUE, C_OFF),
136        '\t- to temporarily disable the check on command line, rebuild with ',
137        '%sRELAX_USES_LIBRARY_CHECK=true%s' % (C_BOLD, C_OFF),
138        ' (this will set compiler filter "verify" and disable AOT-compilation in dexpreopt)\n',
139        '\t- to temporarily disable the check for the whole product, set ',
140        '%sPRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true%s in the product makefiles\n' % (C_BOLD, C_OFF),
141        '\t- to fix the check, make build system properties coherent with the manifest\n',
142        '\t- for details, see %sbuild/make/Changes.md%s' % (C_GREEN, C_OFF),
143        ' and %shttps://source.android.com/devices/tech/dalvik/art-class-loader-context%s\n' % (C_GREEN, C_OFF)
144    ])
145    #pylint: enable=line-too-long
146
147    if not relax:
148        raise ManifestMismatchError(errmsg)
149
150    return errmsg
151
152
153MODULE_NAMESPACE = re.compile('^//[^:]+:')
154
155
156def trim_namespace_parts(modules):
157    """Trim the namespace part of each module, if present.
158
159    Leave only the name.
160    """
161
162    trimmed = []
163    for module in modules:
164        trimmed.append(MODULE_NAMESPACE.sub('', module))
165    return trimmed
166
167
168def extract_uses_libs_apk(badging):
169    """Extract <uses-library> tags from the manifest of an APK."""
170
171    pattern = re.compile("^uses-library(-not-required)?:'(.*)'$", re.MULTILINE)
172
173    required = []
174    optional = []
175    lines = []
176    for match in re.finditer(pattern, badging):
177        lines.append(match.group(0))
178        libname = match.group(2)
179        if match.group(1) is None:
180            required.append(libname)
181        else:
182            optional.append(libname)
183
184    required = first_unique_elements(required)
185    optional = first_unique_elements(optional)
186    tags = first_unique_elements(lines)
187    return required, optional, tags
188
189
190def extract_uses_libs_xml(xml): #pylint: disable=inconsistent-return-statements
191    """Extract <uses-library> tags from the manifest."""
192
193    manifest = parse_manifest(xml)
194    elems = get_children_with_tag(manifest, 'application')
195    application = elems[0] if len(elems) == 1 else None
196    if len(elems) > 1: #pylint: disable=no-else-raise
197        raise RuntimeError('found multiple <application> tags')
198    elif not elems:
199        if uses_libraries or optional_uses_libraries: #pylint: disable=undefined-variable
200            raise ManifestMismatchError('no <application> tag found')
201        return
202
203    libs = get_children_with_tag(application, 'uses-library')
204
205    required = [uses_library_name(x) for x in libs if uses_library_required(x)]
206    optional = [
207        uses_library_name(x) for x in libs if not uses_library_required(x)
208    ]
209
210    # render <uses-library> tags as XML for a pretty error message
211    tags = []
212    for lib in libs:
213        tags.append(lib.toprettyxml())
214
215    required = first_unique_elements(required)
216    optional = first_unique_elements(optional)
217    tags = first_unique_elements(tags)
218    return required, optional, tags
219
220
221def first_unique_elements(l):
222    result = []
223    for x in l:
224        if x not in result:
225            result.append(x)
226    return result
227
228
229def uses_library_name(lib):
230    """Extract the name attribute of a uses-library tag.
231
232  Args:
233    lib: a <uses-library> tag.
234    """
235    name = lib.getAttributeNodeNS(android_ns, 'name')
236    return name.value if name is not None else ''
237
238
239def uses_library_required(lib):
240    """Extract the required attribute of a uses-library tag.
241
242  Args:
243    lib: a <uses-library> tag.
244    """
245    required = lib.getAttributeNodeNS(android_ns, 'required')
246    return (required.value == 'true') if required is not None else True
247
248
249def extract_target_sdk_version(manifest, is_apk=False):
250    """Returns the targetSdkVersion from the manifest.
251
252  Args:
253    manifest: manifest (either parsed XML or aapt dump of APK)
254    is_apk:   if the manifest comes from an APK or an XML file
255    """
256    if is_apk: #pylint: disable=no-else-return
257        return extract_target_sdk_version_apk(manifest)
258    else:
259        return extract_target_sdk_version_xml(manifest)
260
261
262def extract_target_sdk_version_apk(badging):
263    """Extract targetSdkVersion tags from the manifest of an APK."""
264
265    pattern = re.compile("^targetSdkVersion?:'(.*)'$", re.MULTILINE)
266
267    for match in re.finditer(pattern, badging):
268        return match.group(1)
269
270    raise RuntimeError('cannot find targetSdkVersion in the manifest')
271
272
273def extract_target_sdk_version_xml(xml):
274    """Extract targetSdkVersion tags from the manifest."""
275
276    manifest = parse_manifest(xml)
277
278    # Get or insert the uses-sdk element
279    uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
280    if len(uses_sdk) > 1: #pylint: disable=no-else-raise
281        raise RuntimeError('found multiple uses-sdk elements')
282    elif len(uses_sdk) == 0:
283        raise RuntimeError('missing uses-sdk element')
284
285    uses_sdk = uses_sdk[0]
286
287    min_attr = uses_sdk.getAttributeNodeNS(android_ns, 'minSdkVersion')
288    if min_attr is None:
289        raise RuntimeError('minSdkVersion is not specified')
290
291    target_attr = uses_sdk.getAttributeNodeNS(android_ns, 'targetSdkVersion')
292    if target_attr is None:
293        target_attr = min_attr
294
295    return target_attr.value
296
297
298def load_dexpreopt_configs(configs):
299    """Load dexpreopt.config files and map module names to library names."""
300    module_to_libname = {}
301
302    if configs is None:
303        configs = []
304
305    for config in configs:
306        with open(config, 'r') as f:
307            contents = json.load(f)
308        module_to_libname[contents['Name']] = contents['ProvidesUsesLibrary']
309
310    return module_to_libname
311
312
313def translate_libnames(modules, module_to_libname):
314    """Translate module names into library names using the mapping."""
315    if modules is None:
316        modules = []
317
318    libnames = []
319    for name in modules:
320        if name in module_to_libname:
321            name = module_to_libname[name]
322        libnames.append(name)
323
324    return libnames
325
326
327def main():
328    """Program entry point."""
329    try:
330        args = parse_args()
331
332        # The input can be either an XML manifest or an APK, they are parsed and
333        # processed in different ways.
334        is_apk = args.input.endswith('.apk')
335        if is_apk:
336            aapt = args.aapt if args.aapt is not None else 'aapt'
337            manifest = subprocess.check_output(
338                [aapt, 'dump', 'badging', args.input]).decode('utf-8')
339        else:
340            manifest = minidom.parse(args.input)
341
342        if args.enforce_uses_libraries:
343            # Load dexpreopt.config files and build a mapping from module
344            # names to library names. This is necessary because build system
345            # addresses libraries by their module name (`uses_libs`,
346            # `optional_uses_libs`, `LOCAL_USES_LIBRARIES`,
347            # `LOCAL_OPTIONAL_LIBRARY_NAMES` all contain module names), while
348            # the manifest addresses libraries by their name.
349            mod_to_lib = load_dexpreopt_configs(args.dexpreopt_configs)
350            required = translate_libnames(args.uses_libraries, mod_to_lib)
351            optional = translate_libnames(args.optional_uses_libraries,
352                                          mod_to_lib)
353
354            # Check if the <uses-library> lists in the build system agree with
355            # those in the manifest. Raise an exception on mismatch, unless the
356            # script was passed a special parameter to suppress exceptions.
357            errmsg = enforce_uses_libraries(manifest, required, optional,
358                                            args.enforce_uses_libraries_relax,
359                                            is_apk, args.input)
360
361            # Create a status file that is empty on success, or contains an
362            # error message on failure. When exceptions are suppressed,
363            # dexpreopt command command will check file size to determine if
364            # the check has failed.
365            if args.enforce_uses_libraries_status:
366                with open(args.enforce_uses_libraries_status, 'w') as f:
367                    if errmsg is not None:
368                        f.write('%s\n' % errmsg)
369
370        if args.extract_target_sdk_version:
371            try:
372                print(extract_target_sdk_version(manifest, is_apk))
373            except: #pylint: disable=bare-except
374                # Failed; don't crash, return "any" SDK version. This will
375                # result in dexpreopt not adding any compatibility libraries.
376                print(10000)
377
378        if args.output:
379            # XML output is supposed to be written only when this script is
380            # invoked with XML input manifest, not with an APK.
381            if is_apk:
382                raise RuntimeError('cannot save APK manifest as XML')
383
384            with open(args.output, 'w') as f:
385                write_xml(f, manifest)
386
387    # pylint: disable=broad-except
388    except Exception as err:
389        print('%serror:%s ' % (C_RED, C_OFF) + str(err), file=sys.stderr)
390        sys.exit(-1)
391
392
393if __name__ == '__main__':
394    main()
395