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 inserting values from the build system into a manifest.""" 18 19from __future__ import print_function 20import argparse 21import sys 22from xml.dom import minidom 23 24 25android_ns = 'http://schemas.android.com/apk/res/android' 26 27 28def get_children_with_tag(parent, tag_name): 29 children = [] 30 for child in parent.childNodes: 31 if child.nodeType == minidom.Node.ELEMENT_NODE and \ 32 child.tagName == tag_name: 33 children.append(child) 34 return children 35 36 37def find_child_with_attribute(element, tag_name, namespace_uri, 38 attr_name, value): 39 for child in get_children_with_tag(element, tag_name): 40 attr = child.getAttributeNodeNS(namespace_uri, attr_name) 41 if attr is not None and attr.value == value: 42 return child 43 return None 44 45 46def parse_args(): 47 """Parse commandline arguments.""" 48 49 parser = argparse.ArgumentParser() 50 parser.add_argument('--minSdkVersion', default='', dest='min_sdk_version', 51 help='specify minSdkVersion used by the build system') 52 parser.add_argument('--targetSdkVersion', default='', dest='target_sdk_version', 53 help='specify targetSdkVersion used by the build system') 54 parser.add_argument('--raise-min-sdk-version', dest='raise_min_sdk_version', action='store_true', 55 help='raise the minimum sdk version in the manifest if necessary') 56 parser.add_argument('--library', dest='library', action='store_true', 57 help='manifest is for a static library') 58 parser.add_argument('--uses-library', dest='uses_libraries', action='append', 59 help='specify additional <uses-library> tag to add. android:requred is set to true') 60 parser.add_argument('--optional-uses-library', dest='optional_uses_libraries', action='append', 61 help='specify additional <uses-library> tag to add. android:requred is set to false') 62 parser.add_argument('--uses-non-sdk-api', dest='uses_non_sdk_api', action='store_true', 63 help='manifest is for a package built against the platform') 64 parser.add_argument('--use-embedded-dex', dest='use_embedded_dex', action='store_true', 65 help=('specify if the app wants to use embedded dex and avoid extracted,' 66 'locally compiled code. Must not conflict if already declared ' 67 'in the manifest.')) 68 parser.add_argument('--extract-native-libs', dest='extract_native_libs', 69 default=None, type=lambda x: (str(x).lower() == 'true'), 70 help=('specify if the app wants to use embedded native libraries. Must not conflict ' 71 'if already declared in the manifest.')) 72 parser.add_argument('input', help='input AndroidManifest.xml file') 73 parser.add_argument('output', help='output AndroidManifest.xml file') 74 return parser.parse_args() 75 76 77def parse_manifest(doc): 78 """Get the manifest element.""" 79 80 manifest = doc.documentElement 81 if manifest.tagName != 'manifest': 82 raise RuntimeError('expected manifest tag at root') 83 return manifest 84 85 86def ensure_manifest_android_ns(doc): 87 """Make sure the manifest tag defines the android namespace.""" 88 89 manifest = parse_manifest(doc) 90 91 ns = manifest.getAttributeNodeNS(minidom.XMLNS_NAMESPACE, 'android') 92 if ns is None: 93 attr = doc.createAttributeNS(minidom.XMLNS_NAMESPACE, 'xmlns:android') 94 attr.value = android_ns 95 manifest.setAttributeNode(attr) 96 elif ns.value != android_ns: 97 raise RuntimeError('manifest tag has incorrect android namespace ' + 98 ns.value) 99 100 101def as_int(s): 102 try: 103 i = int(s) 104 except ValueError: 105 return s, False 106 return i, True 107 108 109def compare_version_gt(a, b): 110 """Compare two SDK versions. 111 112 Compares a and b, treating codenames like 'Q' as higher 113 than numerical versions like '28'. 114 115 Returns True if a > b 116 117 Args: 118 a: value to compare 119 b: value to compare 120 Returns: 121 True if a is a higher version than b 122 """ 123 124 a, a_is_int = as_int(a.upper()) 125 b, b_is_int = as_int(b.upper()) 126 127 if a_is_int == b_is_int: 128 # Both are codenames or both are versions, compare directly 129 return a > b 130 else: 131 # One is a codename, the other is not. Return true if 132 # b is an integer version 133 return b_is_int 134 135 136def get_indent(element, default_level): 137 indent = '' 138 if element is not None and element.nodeType == minidom.Node.TEXT_NODE: 139 text = element.nodeValue 140 indent = text[:len(text)-len(text.lstrip())] 141 if not indent or indent == '\n': 142 # 1 indent = 4 space 143 indent = '\n' + (' ' * default_level * 4) 144 return indent 145 146 147def raise_min_sdk_version(doc, min_sdk_version, target_sdk_version, library): 148 """Ensure the manifest contains a <uses-sdk> tag with a minSdkVersion. 149 150 Args: 151 doc: The XML document. May be modified by this function. 152 min_sdk_version: The requested minSdkVersion attribute. 153 target_sdk_version: The requested targetSdkVersion attribute. 154 Raises: 155 RuntimeError: invalid manifest 156 """ 157 158 manifest = parse_manifest(doc) 159 160 # Get or insert the uses-sdk element 161 uses_sdk = get_children_with_tag(manifest, 'uses-sdk') 162 if len(uses_sdk) > 1: 163 raise RuntimeError('found multiple uses-sdk elements') 164 elif len(uses_sdk) == 1: 165 element = uses_sdk[0] 166 else: 167 element = doc.createElement('uses-sdk') 168 indent = get_indent(manifest.firstChild, 1) 169 manifest.insertBefore(element, manifest.firstChild) 170 171 # Insert an indent before uses-sdk to line it up with the indentation of the 172 # other children of the <manifest> tag. 173 manifest.insertBefore(doc.createTextNode(indent), manifest.firstChild) 174 175 # Get or insert the minSdkVersion attribute. If it is already present, make 176 # sure it as least the requested value. 177 min_attr = element.getAttributeNodeNS(android_ns, 'minSdkVersion') 178 if min_attr is None: 179 min_attr = doc.createAttributeNS(android_ns, 'android:minSdkVersion') 180 min_attr.value = min_sdk_version 181 element.setAttributeNode(min_attr) 182 else: 183 if compare_version_gt(min_sdk_version, min_attr.value): 184 min_attr.value = min_sdk_version 185 186 # Insert the targetSdkVersion attribute if it is missing. If it is already 187 # present leave it as is. 188 target_attr = element.getAttributeNodeNS(android_ns, 'targetSdkVersion') 189 if target_attr is None: 190 target_attr = doc.createAttributeNS(android_ns, 'android:targetSdkVersion') 191 if library: 192 # TODO(b/117122200): libraries shouldn't set targetSdkVersion at all, but 193 # ManifestMerger treats minSdkVersion="Q" as targetSdkVersion="Q" if it 194 # is empty. Set it to something low so that it will be overriden by the 195 # main manifest, but high enough that it doesn't cause implicit 196 # permissions grants. 197 target_attr.value = '15' 198 else: 199 target_attr.value = target_sdk_version 200 element.setAttributeNode(target_attr) 201 202 203def add_uses_libraries(doc, new_uses_libraries, required): 204 """Add additional <uses-library> tags 205 206 Args: 207 doc: The XML document. May be modified by this function. 208 new_uses_libraries: The names of libraries to be added by this function. 209 required: The value of android:required attribute. Can be true or false. 210 Raises: 211 RuntimeError: Invalid manifest 212 """ 213 214 manifest = parse_manifest(doc) 215 elems = get_children_with_tag(manifest, 'application') 216 application = elems[0] if len(elems) == 1 else None 217 if len(elems) > 1: 218 raise RuntimeError('found multiple <application> tags') 219 elif not elems: 220 application = doc.createElement('application') 221 indent = get_indent(manifest.firstChild, 1) 222 first = manifest.firstChild 223 manifest.insertBefore(doc.createTextNode(indent), first) 224 manifest.insertBefore(application, first) 225 226 indent = get_indent(application.firstChild, 2) 227 228 last = application.lastChild 229 if last is not None and last.nodeType != minidom.Node.TEXT_NODE: 230 last = None 231 232 for name in new_uses_libraries: 233 if find_child_with_attribute(application, 'uses-library', android_ns, 234 'name', name) is not None: 235 # If the uses-library tag of the same 'name' attribute value exists, 236 # respect it. 237 continue 238 239 ul = doc.createElement('uses-library') 240 ul.setAttributeNS(android_ns, 'android:name', name) 241 ul.setAttributeNS(android_ns, 'android:required', str(required).lower()) 242 243 application.insertBefore(doc.createTextNode(indent), last) 244 application.insertBefore(ul, last) 245 246 # align the closing tag with the opening tag if it's not 247 # indented 248 if application.lastChild.nodeType != minidom.Node.TEXT_NODE: 249 indent = get_indent(application.previousSibling, 1) 250 application.appendChild(doc.createTextNode(indent)) 251 252def add_uses_non_sdk_api(doc): 253 """Add android:usesNonSdkApi=true attribute to <application>. 254 255 Args: 256 doc: The XML document. May be modified by this function. 257 Raises: 258 RuntimeError: Invalid manifest 259 """ 260 261 manifest = parse_manifest(doc) 262 elems = get_children_with_tag(manifest, 'application') 263 application = elems[0] if len(elems) == 1 else None 264 if len(elems) > 1: 265 raise RuntimeError('found multiple <application> tags') 266 elif not elems: 267 application = doc.createElement('application') 268 indent = get_indent(manifest.firstChild, 1) 269 first = manifest.firstChild 270 manifest.insertBefore(doc.createTextNode(indent), first) 271 manifest.insertBefore(application, first) 272 273 attr = application.getAttributeNodeNS(android_ns, 'usesNonSdkApi') 274 if attr is None: 275 attr = doc.createAttributeNS(android_ns, 'android:usesNonSdkApi') 276 attr.value = 'true' 277 application.setAttributeNode(attr) 278 279 280def add_use_embedded_dex(doc): 281 manifest = parse_manifest(doc) 282 elems = get_children_with_tag(manifest, 'application') 283 application = elems[0] if len(elems) == 1 else None 284 if len(elems) > 1: 285 raise RuntimeError('found multiple <application> tags') 286 elif not elems: 287 application = doc.createElement('application') 288 indent = get_indent(manifest.firstChild, 1) 289 first = manifest.firstChild 290 manifest.insertBefore(doc.createTextNode(indent), first) 291 manifest.insertBefore(application, first) 292 293 attr = application.getAttributeNodeNS(android_ns, 'useEmbeddedDex') 294 if attr is None: 295 attr = doc.createAttributeNS(android_ns, 'android:useEmbeddedDex') 296 attr.value = 'true' 297 application.setAttributeNode(attr) 298 elif attr.value != 'true': 299 raise RuntimeError('existing attribute mismatches the option of --use-embedded-dex') 300 301 302def add_extract_native_libs(doc, extract_native_libs): 303 manifest = parse_manifest(doc) 304 elems = get_children_with_tag(manifest, 'application') 305 application = elems[0] if len(elems) == 1 else None 306 if len(elems) > 1: 307 raise RuntimeError('found multiple <application> tags') 308 elif not elems: 309 application = doc.createElement('application') 310 indent = get_indent(manifest.firstChild, 1) 311 first = manifest.firstChild 312 manifest.insertBefore(doc.createTextNode(indent), first) 313 manifest.insertBefore(application, first) 314 315 value = str(extract_native_libs).lower() 316 attr = application.getAttributeNodeNS(android_ns, 'extractNativeLibs') 317 if attr is None: 318 attr = doc.createAttributeNS(android_ns, 'android:extractNativeLibs') 319 attr.value = value 320 application.setAttributeNode(attr) 321 elif attr.value != value: 322 raise RuntimeError('existing attribute extractNativeLibs="%s" conflicts with --extract-native-libs="%s"' % 323 (attr.value, value)) 324 325 326def write_xml(f, doc): 327 f.write('<?xml version="1.0" encoding="utf-8"?>\n') 328 for node in doc.childNodes: 329 f.write(node.toxml(encoding='utf-8') + '\n') 330 331 332def main(): 333 """Program entry point.""" 334 try: 335 args = parse_args() 336 337 doc = minidom.parse(args.input) 338 339 ensure_manifest_android_ns(doc) 340 341 if args.raise_min_sdk_version: 342 raise_min_sdk_version(doc, args.min_sdk_version, args.target_sdk_version, args.library) 343 344 if args.uses_libraries: 345 add_uses_libraries(doc, args.uses_libraries, True) 346 347 if args.optional_uses_libraries: 348 add_uses_libraries(doc, args.optional_uses_libraries, False) 349 350 if args.uses_non_sdk_api: 351 add_uses_non_sdk_api(doc) 352 353 if args.use_embedded_dex: 354 add_use_embedded_dex(doc) 355 356 if args.extract_native_libs is not None: 357 add_extract_native_libs(doc, args.extract_native_libs) 358 359 with open(args.output, 'wb') as f: 360 write_xml(f, doc) 361 362 # pylint: disable=broad-except 363 except Exception as err: 364 print('error: ' + str(err), file=sys.stderr) 365 sys.exit(-1) 366 367if __name__ == '__main__': 368 main() 369