123456789101112is_stable_build_tools_basename() {13 build_tools_name=$114 old_ifs=$IFS15 IFS=.16 17 set -- $build_tools_name18 IFS=$old_ifs19 [ "$#" -eq 3 ] || return 120 for segment in "$@"; do21 case "$segment" in22 0|[1-9]|[1-9][0-9]*) ;;23 *) return 1 ;;24 esac25 done26}27282930find_stable_build_tools_dir() {31 sdk_root=$132 build_tools_root=$sdk_root/build-tools33 [ -d "$build_tools_root" ] || return 13435 best_name=36 for candidate in "$build_tools_root"/*; do37 [ -d "$candidate" ] || continue38 candidate_name=${candidate##*/}39 is_stable_build_tools_basename "$candidate_name" || continue40 [ -x "$candidate/aapt2" ] || continue4142 if [ -z "$best_name" ] ||43 [ "$(printf '%s\n' "$best_name" "$candidate_name" | LC_ALL=C sort -V | tail -n 1)" = "$candidate_name" ]; then44 best_name=$candidate_name45 fi46 done4748 [ -n "$best_name" ] || return 149 printf '%s\n' "$build_tools_root/$best_name"50}51525354read_root_semver() {55 version_file=$156 [ -r "$version_file" ] || return 157 root_version=$(sed -n '1p' "$version_file") || return 158 [ -n "$root_version" ] || return 159 is_strict_semver "$root_version" || return 160 61 62 extra_lines=$(sed -n '2,$p' "$version_file" | sed '/^[[:space:]]*$/d')63 [ -z "$extra_lines" ] || return 164 printf '%s\n' "$root_version"65}6667is_strict_semver() {68 semantic_version=$169 case "$semantic_version" in70 ''|*[!0-9.]* ) return 1 ;;71 esac72 old_ifs=$IFS73 IFS=.74 75 set -- $semantic_version76 IFS=$old_ifs77 [ "$#" -eq 3 ] || return 178 for segment in "$@"; do79 case "$segment" in80 0|[1-9]|[1-9][0-9]*) ;;81 *) return 1 ;;82 esac83 done84}858687888990validate_release_metadata() {91 metadata_file=$192 badging_file=$293 root_version=$394 expected_app_id=$495 expected_output=$596 expected_abi=$69798 python3 - "$metadata_file" "$badging_file" "$root_version" "$expected_app_id" "$expected_output" "$expected_abi" <<'PY'99import json100import re101import sys102103metadata_path, badging_path, root_version, expected_app_id, expected_output, expected_abi = sys.argv[1:]104105def reject(message):106 raise SystemExit("release metadata rejected: " + message)107108def strict_semver(value):109 if not isinstance(value, str) or not re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", value):110 reject("version is not strict SemVer")111 for segment in value.split("."):112 if int(segment) > 2147483647:113 reject("version segment is out of range")114115class NoDuplicateKeys(dict):116 def __init__(self, pairs):117 seen = set()118 for key, value in pairs:119 if key in seen:120 reject("duplicate JSON key: " + key)121 seen.add(key)122 self[key] = value123124try:125 with open(metadata_path, encoding="utf-8") as stream:126 metadata = json.load(stream, object_pairs_hook=NoDuplicateKeys)127except (OSError, json.JSONDecodeError) as error:128 reject("malformed Gradle metadata: " + str(error))129130if not isinstance(metadata, dict):131 reject("Gradle metadata root is not an object")132if metadata.get("applicationId") != expected_app_id:133 reject("Gradle applicationId does not match production package")134elements = metadata.get("elements")135if not isinstance(elements, list) or not all(isinstance(item, dict) for item in elements):136 reject("Gradle metadata elements are malformed")137matches = [item for item in elements if item.get("outputFile") == expected_output]138if len(matches) != 1:139 reject("Gradle metadata must contain exactly one matching APK element")140element = matches[0]141if element.get("versionName") != root_version:142 reject("Gradle versionName does not match VERSION")143expected_code = element.get("versionCode")144if isinstance(expected_code, bool) or not isinstance(expected_code, int) or expected_code < 1:145 reject("Gradle versionCode is missing or invalid")146strict_semver(root_version)147148try:149 with open(badging_path, encoding="utf-8") as stream:150 badging = stream.read()151except OSError as error:152 reject("APK badging could not be read: " + str(error))153154package_lines = [line for line in badging.splitlines() if line.startswith("package:")]155if len(package_lines) != 1:156 reject("APK badging must contain exactly one package line")157match = re.fullmatch(158 r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'(?: .*)?",159 package_lines[0],160)161if not match:162 reject("APK badging package line is malformed")163apk_app_id, apk_code, apk_version = match.groups()164if apk_app_id != expected_app_id:165 reject("APK applicationId does not match Gradle metadata")166if apk_code != str(expected_code):167 reject("APK versionCode does not match Gradle metadata")168if apk_version != root_version:169 reject("APK versionName does not match VERSION")170strict_semver(apk_version)171native_lines = [line for line in badging.splitlines() if line.startswith("native-code:")]172if native_lines != [f"native-code: '{expected_abi}'"]:173 reject("APK native ABI does not match requested platform")174PY175}