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