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/apksigner" ] || continue41 [ -x "$candidate/aapt2" ] || continue4243 if [ -z "$best_name" ] ||44 [ "$(printf '%s\n' "$best_name" "$candidate_name" | LC_ALL=C sort -V | tail -n 1)" = "$candidate_name" ]; then45 best_name=$candidate_name46 fi47 done4849 [ -n "$best_name" ] || return 150 printf '%s\n' "$build_tools_root/$best_name"51}52535455read_root_semver() {56 version_file=$157 [ -r "$version_file" ] || return 158 root_version=$(sed -n '1p' "$version_file") || return 159 [ -n "$root_version" ] || return 160 is_strict_semver "$root_version" || return 161 62 63 extra_lines=$(sed -n '2,$p' "$version_file" | sed '/^[[:space:]]*$/d')64 [ -z "$extra_lines" ] || return 165 printf '%s\n' "$root_version"66}6768is_strict_semver() {69 semantic_version=$170 case "$semantic_version" in71 ''|*[!0-9.]* ) return 1 ;;72 esac73 old_ifs=$IFS74 IFS=.75 76 set -- $semantic_version77 IFS=$old_ifs78 [ "$#" -eq 3 ] || return 179 for segment in "$@"; do80 case "$segment" in81 0|[1-9]|[1-9][0-9]*) ;;82 *) return 1 ;;83 esac84 done85}868788899091validate_release_metadata() {92 metadata_file=$193 badging_file=$294 root_version=$395 expected_app_id=$49697 python3 - "$metadata_file" "$badging_file" "$root_version" "$expected_app_id" <<'PY'98import json99import re100import sys101102metadata_path, badging_path, root_version, expected_app_id = sys.argv[1:]103104def reject(message):105 raise SystemExit("release metadata rejected: " + message)106107def strict_semver(value):108 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):109 reject("version is not strict SemVer")110 for segment in value.split("."):111 if int(segment) > 2147483647:112 reject("version segment is out of range")113114class NoDuplicateKeys(dict):115 def __init__(self, pairs):116 seen = set()117 for key, value in pairs:118 if key in seen:119 reject("duplicate JSON key: " + key)120 seen.add(key)121 self[key] = value122123try:124 with open(metadata_path, encoding="utf-8") as stream:125 metadata = json.load(stream, object_pairs_hook=NoDuplicateKeys)126except (OSError, json.JSONDecodeError) as error:127 reject("malformed Gradle metadata: " + str(error))128129if not isinstance(metadata, dict):130 reject("Gradle metadata root is not an object")131if metadata.get("applicationId") != expected_app_id:132 reject("Gradle applicationId does not match production package")133elements = metadata.get("elements")134if not isinstance(elements, list) or len(elements) != 1 or not isinstance(elements[0], dict):135 reject("Gradle metadata must contain exactly one APK element")136element = elements[0]137if element.get("versionName") != root_version:138 reject("Gradle versionName does not match VERSION")139expected_code = element.get("versionCode")140if isinstance(expected_code, bool) or not isinstance(expected_code, int) or expected_code < 1:141 reject("Gradle versionCode is missing or invalid")142strict_semver(root_version)143144try:145 with open(badging_path, encoding="utf-8") as stream:146 badging = stream.read()147except OSError as error:148 reject("APK badging could not be read: " + str(error))149150package_lines = [line for line in badging.splitlines() if line.startswith("package:")]151if len(package_lines) != 1:152 reject("APK badging must contain exactly one package line")153match = re.fullmatch(154 r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'(?: .*)?",155 package_lines[0],156)157if not match:158 reject("APK badging package line is malformed")159apk_app_id, apk_code, apk_version = match.groups()160if apk_app_id != expected_app_id:161 reject("APK applicationId does not match Gradle metadata")162if apk_code != str(expected_code):163 reject("APK versionCode does not match Gradle metadata")164if apk_version != root_version:165 reject("APK versionName does not match VERSION")166strict_semver(apk_version)167PY168}