manifest_check.py 14 KB

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