kepr-build-helpers.sh

5.9 KB · raw

1#!/bin/sh2# Sourceable, secret-free helpers for the Kepr release gate.3#4# This file intentionally contains no signing or Gradle commands. It only5# selects a stable SDK tool directory and validates public release metadata so6# the publication script and its hermetic negative-path tests share one7# implementation.89# Return success only for a stable build-tools directory basename such as10# "35.0.0". Preview/RC/beta/canary labels, "latest", leading-zero segments,11# and arbitrary names are rejected before any version sorting occurs.12is_stable_build_tools_basename() {13    build_tools_name=$114    old_ifs=$IFS15    IFS=.16    # shellcheck disable=SC208617    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}2728# Print the newest stable build-tools directory containing both tools. Only29# basenames that passed is_stable_build_tools_basename reach sort -V.30find_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}5253# Read and validate the one-line semantic version authority. Keep this in the54# shared helper so publication tests exercise the exact same root-file gate.55read_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    # A second non-empty line would make VERSION ambiguous even if the first62    # line happened to be valid.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    # shellcheck disable=SC208676    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}8687# Validate Gradle output-metadata.json, the APK's aapt2 badging, and VERSION as88# one release identity. The Python standard library is used only for parsing89# generated JSON; duplicate keys and ambiguous element lists are rejected.90# Arguments: metadata-json badging-file root-version expected-app-id91validate_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}