1# pylint: disable=g-direct-third-party-import 2# Copyright 2022 The Bazel Authors. All rights reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""AndroidManifest tool to enforce a floor on the minSdkVersion attribute. 16 17Ensures that the minSdkVersion attribute is >= than the specified floor, 18and if the attribute is either not specified or less than the floor, 19sets it to the floor. 20""" 21 22import os 23import sys 24 25import xml.etree.ElementTree as ET 26from absl import app 27from absl import flags 28 29BUMP = "bump" 30VALIDATE = "validate" 31SET_DEFAULT = "set_default" 32 33USES_SDK = "uses-sdk" 34MIN_SDK_ATTRIB = "{http://schemas.android.com/apk/res/android}minSdkVersion" 35 36FLAGS = flags.FLAGS 37 38flags.DEFINE_enum( 39 "action", 40 None, 41 [BUMP, VALIDATE, SET_DEFAULT], 42 f"Action to perform, either {BUMP}, {VALIDATE}, or {SET_DEFAULT}") 43flags.DEFINE_string( 44 "manifest", 45 None, 46 "AndroidManifest.xml of the instrumentation APK") 47flags.DEFINE_integer( 48 "min_sdk_floor", 49 0, 50 "Min SDK floor", 51 lower_bound=0) 52# Needed for SET_DEFAULT 53flags.DEFINE_string( 54 "default_min_sdk", 55 None, 56 "Default min SDK") 57# Needed for BUMP and SET_DEFAULT 58flags.DEFINE_string( 59 "output", 60 None, 61 f"Output AndroidManifest.xml to generate, only needed for {BUMP}") 62flags.DEFINE_string("log", None, "Path to write the log to") 63 64 65class MinSdkError(Exception): 66 """Raised when there is a problem with the min SDK attribute in AndroidManifest.xml.""" 67 68 69def ParseNamespaces(xml_content): 70 """Parse namespaces first to keep the prefix. 71 72 Args: 73 xml_content: str, the contents of the AndroidManifest.xml file 74 """ 75 # Always register the android namespace first. This will be overriden by 76 # any other definition in the manifest. 77 ET.register_namespace("android", "http://schemas.android.com/apk/res/android") 78 ns_parser = ET.XMLPullParser(events=["start-ns"]) 79 ns_parser.feed(xml_content) 80 ns_parser.close() 81 for _, ns_tuple in ns_parser.read_events(): 82 try: 83 ET.register_namespace(ns_tuple[0], ns_tuple[1]) 84 except ValueError: 85 pass 86 87 88def _BumpMinSdk(xml_content, min_sdk_floor): 89 """Checks the min SDK in xml_content and replaces with min_sdk_floor if needed. 90 91 Args: 92 xml_content: str, the contents of the AndroidManifest.xml file 93 min_sdk_floor: int, the min SDK floor 94 95 Returns: 96 A tuple with the following elements: 97 - str: The xml contents of the manifest with the min SDK floor enforced. 98 This string will be equal to the input if the min SDK is already not less 99 than the floor. 100 - str: log message of action taken 101 """ 102 if min_sdk_floor == 0: 103 return xml_content, "No min SDK floor specified. Manifest unchanged." 104 105 ParseNamespaces(xml_content) 106 107 root = ET.fromstring(xml_content) 108 uses_sdk = root.find(USES_SDK) 109 if uses_sdk is None: 110 ET.SubElement(root, USES_SDK, {MIN_SDK_ATTRIB: str(min_sdk_floor)}) 111 return ( 112 ET.tostring(root, encoding="utf-8", xml_declaration=True), 113 "No uses-sdk element found while floor is specified " 114 + f"({min_sdk_floor}). Min SDK added.") 115 116 min_sdk = uses_sdk.get(MIN_SDK_ATTRIB) 117 if min_sdk is None: 118 uses_sdk.set(MIN_SDK_ATTRIB, str(min_sdk_floor)) 119 return ( 120 ET.tostring(root, encoding="utf-8", xml_declaration=True), 121 "No minSdkVersion attribute found while floor is specified" 122 + f"({min_sdk_floor}). Min SDK added.") 123 124 try: 125 min_sdk_int = int(min_sdk) 126 except ValueError: 127 return ( 128 xml_content, 129 f"Placeholder used for the minSdkVersion attribute ({min_sdk}). " 130 + "Manifest unchanged.") 131 132 if min_sdk_int < min_sdk_floor: 133 uses_sdk.set(MIN_SDK_ATTRIB, str(min_sdk_floor)) 134 return ( 135 ET.tostring(root, encoding="utf-8", xml_declaration=True), 136 f"minSdkVersion attribute specified in the manifest ({min_sdk}) " 137 + f"is less than the floor ({min_sdk_floor}). Min SDK replaced.") 138 return ( 139 xml_content, 140 f"minSdkVersion attribute specified in the manifest ({min_sdk}) " 141 + f"is not less than the floor ({min_sdk_floor}). Manifest unchanged.") 142 143 144def _ValidateMinSdk(xml_content, min_sdk_floor): 145 """Checks the min SDK in xml_content and raises MinSdkError if it is either not specified or less than the floor. 146 147 Args: 148 xml_content: str, the contents of the AndroidManifest.xml file 149 min_sdk_floor: int, the min SDK floor 150 Returns: 151 str: log message 152 Raises: 153 MinSdkError: The min SDK is less than the specified floor. 154 """ 155 if min_sdk_floor == 0: 156 return "No min SDK floor specified." 157 158 root = ET.fromstring(xml_content) 159 160 uses_sdk = root.find(USES_SDK) 161 if uses_sdk is None: 162 raise MinSdkError( 163 "No uses-sdk element found in manifest " 164 + f"while floor is specified ({min_sdk_floor}).") 165 166 min_sdk = uses_sdk.get(MIN_SDK_ATTRIB) 167 if min_sdk is None: 168 raise MinSdkError( 169 "No minSdkVersion attribute found in manifest " 170 + f"while floor is specified ({min_sdk_floor}).") 171 172 try: 173 min_sdk_int = int(min_sdk) 174 except ValueError: 175 return f"Placeholder minSdkVersion = {min_sdk}\n min SDK floor = {min_sdk_floor}" 176 177 if min_sdk_int < min_sdk_floor: 178 raise MinSdkError( 179 f"minSdkVersion attribute specified in the manifest ({min_sdk}) " 180 + f"is less than the floor ({min_sdk_floor}).") 181 return f"minSdkVersion = {min_sdk}\n min SDK floor = {min_sdk_floor}" 182 183 184def _SetDefaultMinSdk(xml_content, default_min_sdk): 185 """Checks the min SDK in xml_content and replaces with default_min_sdk if it is not already set. 186 187 Args: 188 xml_content: str, the contents of the AndroidManifest.xml file 189 default_min_sdk: str, can be set to either a number or an unreleased version 190 full name 191 192 Returns: 193 A tuple with the following elements: 194 - str: The xml contents of the manifest with the min SDK floor enforced. 195 This string will be equal to the input if the min SDK is already set. 196 - str: log message of action taken 197 """ 198 if default_min_sdk is None: 199 return xml_content, ("No default min SDK floor specified. Manifest " 200 "unchanged.") 201 202 ParseNamespaces(xml_content) 203 204 root = ET.fromstring(xml_content) 205 uses_sdk = root.find(USES_SDK) 206 if uses_sdk is None: 207 ET.SubElement(root, USES_SDK, {MIN_SDK_ATTRIB: default_min_sdk}) 208 return ( 209 ET.tostring(root, encoding="utf-8", xml_declaration=True), 210 "No uses-sdk element found while default is specified. " 211 + f"Min SDK ({default_min_sdk}) added.") 212 213 min_sdk = uses_sdk.get(MIN_SDK_ATTRIB) 214 if min_sdk is None: 215 uses_sdk.set(MIN_SDK_ATTRIB, str(default_min_sdk)) 216 return ( 217 ET.tostring(root, encoding="utf-8", xml_declaration=True), 218 "No minSdkVersion attribute found while default is specified" 219 + f"({default_min_sdk}). Min SDK set to default.") 220 221 return ( 222 xml_content, 223 f"minSdkVersion attribute specified in the manifest ({min_sdk}) " 224 + ". Manifest unchanged.") 225 226 227def main(unused_argv): 228 manifest_path = FLAGS.manifest 229 with open(manifest_path, "rb") as f: 230 manifest = f.read() 231 232 if FLAGS.action == BUMP: 233 output_path = FLAGS.output 234 dirname = os.path.dirname(output_path) 235 if not os.path.exists(dirname): 236 os.makedirs(dirname) 237 238 out_contents, log_message = _BumpMinSdk(manifest, FLAGS.min_sdk_floor) 239 with open(output_path, "wb") as f: 240 f.write(out_contents) 241 242 elif FLAGS.action == SET_DEFAULT: 243 output_path = FLAGS.output 244 dirname = os.path.dirname(output_path) 245 if not os.path.exists(dirname): 246 os.makedirs(dirname) 247 248 out_contents, log_message = _SetDefaultMinSdk( 249 manifest, FLAGS.default_min_sdk 250 ) 251 with open(output_path, "wb") as f: 252 f.write(out_contents) 253 254 elif FLAGS.action == VALIDATE: 255 try: 256 log_message = _ValidateMinSdk(manifest, FLAGS.min_sdk_floor) 257 except MinSdkError as e: 258 sys.exit(str(e)) 259 else: 260 sys.exit(f"Action must be either {BUMP} or {VALIDATE}") 261 262 if FLAGS.log is not None: 263 log_path = FLAGS.log 264 dirname = os.path.dirname(log_path) 265 if not os.path.exists(dirname): 266 os.makedirs(dirname) 267 with open(log_path, "w") as f: 268 f.write(log_message) 269 270if __name__ == "__main__": 271 app.run(main) 272