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
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')
54  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)')
61  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')
65  parser.add_argument('--dexpreopt-config',
66                      dest='dexpreopt_configs',
67                      action='append',
68                      help='a paths to a dexpreopt.config of some library')
69  parser.add_argument('--aapt',
70                      dest='aapt',
71                      help='path to aapt executable')
72  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
77def enforce_uses_libraries(manifest, required, optional, relax, is_apk, path):
78  """Verify that the <uses-library> tags in the manifest match those provided
79  by the build system.
80
81  Args:
82    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
87  """
88  if is_apk:
89    manifest_required, manifest_optional, tags = extract_uses_libs_apk(manifest)
90  else:
91    manifest_required, manifest_optional, tags = extract_uses_libs_xml(manifest)
92
93  if manifest_required == required and manifest_optional == optional:
94    return None
95
96  errmsg = ''.join([
97    'mismatch in the <uses-library> tags between the build system and the '
98      'manifest:\n',
99    '\t- required libraries in build system: [%s]\n' % ', '.join(required),
100    '\t                 vs. in the manifest: [%s]\n' % ', '.join(manifest_required),
101    '\t- optional libraries in build system: [%s]\n' % ', '.join(optional),
102    '\t                 vs. in the manifest: [%s]\n' % ', '.join(manifest_optional),
103    '\t- tags in the manifest (%s):\n' % path,
104    '\t\t%s\n' % '\t\t'.join(tags),
105      'note: the following options are available:\n',
106    '\t- to temporarily disable the check on command line, rebuild with ',
107      'RELAX_USES_LIBRARY_CHECK=true (this will set compiler filter "verify" ',
108      'and disable AOT-compilation in dexpreopt)\n',
109    '\t- to temporarily disable the check for the whole product, set ',
110      'PRODUCT_BROKEN_VERIFY_USES_LIBRARIES := true in the product makefiles\n',
111    '\t- to fix the check, make build system properties coherent with the '
112      'manifest\n',
113    '\t- see build/make/Changes.md for details\n'])
114
115  if not relax:
116    raise ManifestMismatchError(errmsg)
117
118  return errmsg
119
120
121def extract_uses_libs_apk(badging):
122  """Extract <uses-library> tags from the manifest of an APK."""
123
124  pattern = re.compile("^uses-library(-not-required)?:'(.*)'$", re.MULTILINE)
125
126  required = []
127  optional = []
128  lines = []
129  for match in re.finditer(pattern, badging):
130    lines.append(match.group(0))
131    libname = match.group(2)
132    if match.group(1) == None:
133      required.append(libname)
134    else:
135      optional.append(libname)
136
137  required = first_unique_elements(required)
138  optional = first_unique_elements(optional)
139  tags = first_unique_elements(lines)
140  return required, optional, tags
141
142
143def extract_uses_libs_xml(xml):
144  """Extract <uses-library> tags from the manifest."""
145
146  manifest = parse_manifest(xml)
147  elems = get_children_with_tag(manifest, 'application')
148  application = elems[0] if len(elems) == 1 else None
149  if len(elems) > 1:
150    raise RuntimeError('found multiple <application> tags')
151  elif not elems:
152    if uses_libraries or optional_uses_libraries:
153      raise ManifestMismatchError('no <application> tag found')
154    return
155
156  libs = get_children_with_tag(application, 'uses-library')
157
158  required = [uses_library_name(x) for x in libs if uses_library_required(x)]
159  optional = [uses_library_name(x) for x in libs if not uses_library_required(x)]
160
161  # render <uses-library> tags as XML for a pretty error message
162  tags = []
163  for lib in libs:
164    tags.append(lib.toprettyxml())
165
166  required = first_unique_elements(required)
167  optional = first_unique_elements(optional)
168  tags = first_unique_elements(tags)
169  return required, optional, tags
170
171
172def first_unique_elements(l):
173  result = []
174  [result.append(x) for x in l if x not in result]
175  return result
176
177
178def uses_library_name(lib):
179  """Extract the name attribute of a uses-library tag.
180
181  Args:
182    lib: a <uses-library> tag.
183  """
184  name = lib.getAttributeNodeNS(android_ns, 'name')
185  return name.value if name is not None else ""
186
187
188def uses_library_required(lib):
189  """Extract the required attribute of a uses-library tag.
190
191  Args:
192    lib: a <uses-library> tag.
193  """
194  required = lib.getAttributeNodeNS(android_ns, 'required')
195  return (required.value == 'true') if required is not None else True
196
197
198def extract_target_sdk_version(manifest, is_apk = False):
199  """Returns the targetSdkVersion from the manifest.
200
201  Args:
202    manifest: manifest (either parsed XML or aapt dump of APK)
203    is_apk:   if the manifest comes from an APK or an XML file
204  """
205  if is_apk:
206    return extract_target_sdk_version_apk(manifest)
207  else:
208    return extract_target_sdk_version_xml(manifest)
209
210
211def extract_target_sdk_version_apk(badging):
212  """Extract targetSdkVersion tags from the manifest of an APK."""
213
214  pattern = re.compile("^targetSdkVersion?:'(.*)'$", re.MULTILINE)
215
216  for match in re.finditer(pattern, badging):
217    return match.group(1)
218
219  raise RuntimeError('cannot find targetSdkVersion in the manifest')
220
221
222def extract_target_sdk_version_xml(xml):
223  """Extract targetSdkVersion tags from the manifest."""
224
225  manifest = parse_manifest(xml)
226
227  # Get or insert the uses-sdk element
228  uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
229  if len(uses_sdk) > 1:
230    raise RuntimeError('found multiple uses-sdk elements')
231  elif len(uses_sdk) == 0:
232    raise RuntimeError('missing uses-sdk element')
233
234  uses_sdk = uses_sdk[0]
235
236  min_attr = uses_sdk.getAttributeNodeNS(android_ns, 'minSdkVersion')
237  if min_attr is None:
238    raise RuntimeError('minSdkVersion is not specified')
239
240  target_attr = uses_sdk.getAttributeNodeNS(android_ns, 'targetSdkVersion')
241  if target_attr is None:
242    target_attr = min_attr
243
244  return target_attr.value
245
246
247def load_dexpreopt_configs(configs):
248  """Load dexpreopt.config files and map module names to library names."""
249  module_to_libname = {}
250
251  if configs is None:
252    configs = []
253
254  for config in configs:
255    with open(config, 'r') as f:
256      contents = json.load(f)
257    module_to_libname[contents['Name']] = contents['ProvidesUsesLibrary']
258
259  return module_to_libname
260
261
262def translate_libnames(modules, module_to_libname):
263  """Translate module names into library names using the mapping."""
264  if modules is None:
265    modules = []
266
267  libnames = []
268  for name in modules:
269    if name in module_to_libname:
270      name = module_to_libname[name]
271    libnames.append(name)
272
273  return libnames
274
275
276def main():
277  """Program entry point."""
278  try:
279    args = parse_args()
280
281    # The input can be either an XML manifest or an APK, they are parsed and
282    # processed in different ways.
283    is_apk = args.input.endswith('.apk')
284    if is_apk:
285      aapt = args.aapt if args.aapt != None else "aapt"
286      manifest = subprocess.check_output([aapt, "dump", "badging", args.input])
287    else:
288      manifest = minidom.parse(args.input)
289
290    if args.enforce_uses_libraries:
291      # Load dexpreopt.config files and build a mapping from module names to
292      # library names. This is necessary because build system addresses
293      # libraries by their module name (`uses_libs`, `optional_uses_libs`,
294      # `LOCAL_USES_LIBRARIES`, `LOCAL_OPTIONAL_LIBRARY_NAMES` all contain
295      # module names), while the manifest addresses libraries by their name.
296      mod_to_lib = load_dexpreopt_configs(args.dexpreopt_configs)
297      required = translate_libnames(args.uses_libraries, mod_to_lib)
298      optional = translate_libnames(args.optional_uses_libraries, mod_to_lib)
299
300      # Check if the <uses-library> lists in the build system agree with those
301      # in the manifest. Raise an exception on mismatch, unless the script was
302      # passed a special parameter to suppress exceptions.
303      errmsg = enforce_uses_libraries(manifest, required, optional,
304        args.enforce_uses_libraries_relax, is_apk, args.input)
305
306      # Create a status file that is empty on success, or contains an error
307      # message on failure. When exceptions are suppressed, dexpreopt command
308      # command will check file size to determine if the check has failed.
309      if args.enforce_uses_libraries_status:
310        with open(args.enforce_uses_libraries_status, 'w') as f:
311          if not errmsg == None:
312            f.write("%s\n" % errmsg)
313
314    if args.extract_target_sdk_version:
315      try:
316        print(extract_target_sdk_version(manifest, is_apk))
317      except:
318        # Failed; don't crash, return "any" SDK version. This will result in
319        # dexpreopt not adding any compatibility libraries.
320        print(10000)
321
322    if args.output:
323      # XML output is supposed to be written only when this script is invoked
324      # with XML input manifest, not with an APK.
325      if is_apk:
326        raise RuntimeError('cannot save APK manifest as XML')
327
328      with open(args.output, 'wb') as f:
329        write_xml(f, manifest)
330
331  # pylint: disable=broad-except
332  except Exception as err:
333    print('error: ' + str(err), file=sys.stderr)
334    sys.exit(-1)
335
336if __name__ == '__main__':
337  main()
338