diff --git a/core/definitions.mk b/core/definitions.mk index df63f7cd2..335c6486d 100644 --- a/core/definitions.mk +++ b/core/definitions.mk @@ -2300,11 +2300,21 @@ define add-carried-jack-resources fi endef +# Returns the minSdkVersion of the specified APK as a decimal number. If the +# version is a codename, returns the current platform SDK version (always a +# decimal number) instead. +# +define get-package-min-sdk-version-int +$$($(AAPT) dump badging $(1) 2>&1 | grep '^sdkVersion' | cut -d"'" -f2 | \ + sed -e s/^$(PLATFORM_VERSION_CODENAME)$$/$(PLATFORM_SDK_VERSION)/) +endef + # Sign a package using the specified key/cert. # define sign-package $(hide) mv $@ $@.unsigned $(hide) java -Djava.library.path=$(SIGNAPK_JNI_LIBRARY_PATH) -jar $(SIGNAPK_JAR) \ + --min-sdk-version $(call get-package-min-sdk-version-int,$@.unsigned) \ $(PRIVATE_CERTIFICATE) $(PRIVATE_PRIVATE_KEY) \ $(PRIVATE_ADDITIONAL_CERTIFICATES) $@.unsigned $@.signed $(hide) mv $@.signed $@ diff --git a/core/prebuilt_internal.mk b/core/prebuilt_internal.mk index dd8ff1cf8..9a2ca1174 100644 --- a/core/prebuilt_internal.mk +++ b/core/prebuilt_internal.mk @@ -213,7 +213,7 @@ embedded_prebuilt_jni_libs := 'lib/*.so' endif $(built_module): PRIVATE_EMBEDDED_JNI_LIBS := $(embedded_prebuilt_jni_libs) -$(built_module) : $(my_prebuilt_src_file) | $(ACP) $(ZIPALIGN) $(SIGNAPK_JAR) +$(built_module) : $(my_prebuilt_src_file) | $(ACP) $(ZIPALIGN) $(SIGNAPK_JAR) $(AAPT) $(transform-prebuilt-to-target) $(uncompress-shared-libs) ifneq ($(LOCAL_CERTIFICATE),PRESIGNED) @@ -252,7 +252,7 @@ my_src_dir := $(LOCAL_PATH)/$(my_src_dir) $(built_apk_splits) : PRIVATE_PRIVATE_KEY := $(LOCAL_CERTIFICATE).pk8 $(built_apk_splits) : PRIVATE_CERTIFICATE := $(LOCAL_CERTIFICATE).x509.pem -$(built_apk_splits) : $(built_module_path)/%.apk : $(my_src_dir)/%.apk | $(ACP) +$(built_apk_splits) : $(built_module_path)/%.apk : $(my_src_dir)/%.apk | $(ACP) $(AAPT) $(copy-file-to-new-target) $(sign-package) diff --git a/tools/releasetools/common.py b/tools/releasetools/common.py index 95aeb6268..664a7d3e0 100644 --- a/tools/releasetools/common.py +++ b/tools/releasetools/common.py @@ -592,7 +592,46 @@ def GetKeyPasswords(keylist): return key_passwords -def SignFile(input_name, output_name, key, password, whole_file=False): +def GetMinSdkVersion(apk_name): + """Get the minSdkVersion delared in the APK. This can be both a decimal number + (API Level) or a codename. + """ + + p = Run(["aapt", "dump", "badging", apk_name], stdout=subprocess.PIPE) + output, err = p.communicate() + if err: + raise ExternalError("Failed to obtain minSdkVersion: aapt return code %s" + % (p.returncode,)) + + for line in output.split("\n"): + # Looking for lines such as sdkVersion:'23' or sdkVersion:'M' + m = re.match(r'sdkVersion:\'([^\']*)\'', line) + if m: + return m.group(1) + raise ExternalError("No minSdkVersion returned by aapt") + + +def GetMinSdkVersionInt(apk_name, codename_to_api_level_map): + """Get the minSdkVersion declared in the APK as a number (API Level). If + minSdkVersion is set to a codename, it is translated to a number using the + provided map. + """ + + version = GetMinSdkVersion(apk_name) + try: + return int(version) + except ValueError: + # Not a decimal number. Codename? + if version in codename_to_api_level_map: + return codename_to_api_level_map[version] + else: + raise ExternalError("Unknown minSdkVersion: '%s'. Known codenames: %s" + % (version, codename_to_api_level_map)) + + +def SignFile(input_name, output_name, key, password, min_api_level=None, + codename_to_api_level_map=dict(), + whole_file=False): """Sign the input_name zip/jar/apk, producing output_name. Use the given key and password (the latter may be None if the key does not have a password. @@ -600,6 +639,13 @@ def SignFile(input_name, output_name, key, password, whole_file=False): If whole_file is true, use the "-w" option to SignApk to embed a signature that covers the whole file in the archive comment of the zip file. + + min_api_level is the API Level (int) of the oldest platform this file may end + up on. If not specified for an APK, the API Level is obtained by interpreting + the minSdkVersion attribute of the APK's AndroidManifest.xml. + + codename_to_api_level_map is needed to translate the codename which may be + encountered as the APK's minSdkVersion. """ java_library_path = os.path.join( @@ -612,6 +658,15 @@ def SignFile(input_name, output_name, key, password, whole_file=False): cmd.extend(OPTIONS.extra_signapk_args) if whole_file: cmd.append("-w") + + min_sdk_version = min_api_level + if min_sdk_version is None: + if not whole_file: + min_sdk_version = GetMinSdkVersionInt( + input_name, codename_to_api_level_map) + if min_sdk_version is not None: + cmd.extend(["--min-sdk-version", str(min_sdk_version)]) + cmd.extend([key + OPTIONS.public_key_suffix, key + OPTIONS.private_key_suffix, input_name, output_name]) diff --git a/tools/releasetools/sign_target_files_apks.py b/tools/releasetools/sign_target_files_apks.py index baf60f5c6..8941e3569 100755 --- a/tools/releasetools/sign_target_files_apks.py +++ b/tools/releasetools/sign_target_files_apks.py @@ -127,14 +127,34 @@ def CheckAllApksSigned(input_tf_zip, apk_key_map): sys.exit(1) -def SignApk(data, keyname, pw): +def SignApk(data, keyname, pw, platform_api_level, codename_to_api_level_map): unsigned = tempfile.NamedTemporaryFile() unsigned.write(data) unsigned.flush() signed = tempfile.NamedTemporaryFile() - common.SignFile(unsigned.name, signed.name, keyname, pw) + # For pre-N builds, don't upgrade to SHA-256 JAR signatures based on the APK's + # minSdkVersion to avoid increasing incremental OTA update sizes. If an APK + # didn't change, we don't want its signature to change due to the switch + # from SHA-1 to SHA-256. + # By default, APK signer chooses SHA-256 signatures if the APK's minSdkVersion + # is 18 or higher. For pre-N builds we disable this mechanism by pretending + # that the APK's minSdkVersion is 1. + # For N+ builds, we let APK signer rely on the APK's minSdkVersion to + # determine whether to use SHA-256. + min_api_level = None + if platform_api_level > 23: + # Let APK signer choose whether to use SHA-1 or SHA-256, based on the APK's + # minSdkVersion attribute + min_api_level = None + else: + # Force APK signer to use SHA-1 + min_api_level = 1 + + common.SignFile(unsigned.name, signed.name, keyname, pw, + min_api_level=min_api_level, + codename_to_api_level_map=codename_to_api_level_map) data = signed.read() unsigned.close() @@ -144,7 +164,8 @@ def SignApk(data, keyname, pw): def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, - apk_key_map, key_passwords): + apk_key_map, key_passwords, platform_api_level, + codename_to_api_level_map): maxsize = max([len(os.path.basename(i.filename)) for i in input_tf_zip.infolist() @@ -200,7 +221,8 @@ def ProcessTargetFiles(input_tf_zip, output_tf_zip, misc_info, key = apk_key_map[name] if key not in common.SPECIAL_CERT_STRINGS: print " signing: %-*s (%s)" % (maxsize, name, key) - signed_data = SignApk(data, key, key_passwords[key]) + signed_data = SignApk(data, key, key_passwords[key], platform_api_level, + codename_to_api_level_map) common.ZipWriteStr(output_tf_zip, out_info, signed_data) else: # an APK we're not supposed to sign. @@ -440,6 +462,57 @@ def BuildKeyMap(misc_info, key_mapping_options): OPTIONS.key_map[s] = d +def GetApiLevelAndCodename(input_tf_zip): + data = input_tf_zip.read("SYSTEM/build.prop") + api_level = None + codename = None + for line in data.split("\n"): + line = line.strip() + original_line = line + if line and line[0] != '#' and "=" in line: + key, value = line.split("=", 1) + key = key.strip() + if key == "ro.build.version.sdk": + api_level = int(value.strip()) + elif key == "ro.build.version.codename": + codename = value.strip() + + if api_level is None: + raise ValueError("No ro.build.version.sdk in SYSTEM/build.prop") + if codename is None: + raise ValueError("No ro.build.version.codename in SYSTEM/build.prop") + + return (api_level, codename) + + +def GetCodenameToApiLevelMap(input_tf_zip): + data = input_tf_zip.read("SYSTEM/build.prop") + api_level = None + codenames = None + for line in data.split("\n"): + line = line.strip() + original_line = line + if line and line[0] != '#' and "=" in line: + key, value = line.split("=", 1) + key = key.strip() + if key == "ro.build.version.sdk": + api_level = int(value.strip()) + elif key == "ro.build.version.all_codenames": + codenames = value.strip().split(",") + + if api_level is None: + raise ValueError("No ro.build.version.sdk in SYSTEM/build.prop") + if codenames is None: + raise ValueError("No ro.build.version.all_codenames in SYSTEM/build.prop") + + result = dict() + for codename in codenames: + codename = codename.strip() + if len(codename) > 0: + result[codename] = api_level + return result + + def main(argv): key_mapping_options = [] @@ -498,8 +571,17 @@ def main(argv): CheckAllApksSigned(input_zip, apk_key_map) key_passwords = common.GetKeyPasswords(set(apk_key_map.values())) + platform_api_level, platform_codename = GetApiLevelAndCodename(input_zip) + codename_to_api_level_map = GetCodenameToApiLevelMap(input_zip) + # Android N will be API Level 24, but isn't yet. + # TODO: Remove this workaround once Android N is officially API Level 24. + if platform_api_level == 23 and platform_codename == "N": + platform_api_level = 24 + ProcessTargetFiles(input_zip, output_zip, misc_info, - apk_key_map, key_passwords) + apk_key_map, key_passwords, + platform_api_level, + codename_to_api_level_map) common.ZipClose(input_zip) common.ZipClose(output_zip)