#!/bin/sh # Build and publish the release APK for Kepr Builds. # # Kepr Beta builds use the established release certificate. Validate the produced # APK's package, version, ABI, and signing identity before staging it. set -eu # PB-26-008: tool paths are discovered from the environment first, then a # Homebrew default, then fail closed. No silent fallback to an unknown path. JAVA_HOME="${PEACH_JAVA_HOME:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}" if [ ! -d "$JAVA_HOME" ]; then echo "refusing to publish: Java 17 runtime not found at $JAVA_HOME (set PEACH_JAVA_HOME)" >&2 exit 1 fi export JAVA_HOME ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT_DIR" # Keep the tool-selection and public metadata gates shared with the hermetic # negative-path test; this script owns build orchestration only. # shellcheck source=scripts/kepr-build-helpers.sh . "$ROOT_DIR/scripts/kepr-build-helpers.sh" # Resolve the SDK from the environment first, then the non-secret sdk.dir in # local.properties. Pick the newest *versioned* build-tools directory containing both # tools; build-tools/latest is intentionally never trusted or consulted. SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" if [ -z "$SDK_ROOT" ]; then SDK_ROOT=$(sed -n 's/^sdk\.dir=//p' "$ROOT_DIR/local.properties" 2>/dev/null | sed -n '1p') fi if [ -z "$SDK_ROOT" ] || [ ! -d "$SDK_ROOT/build-tools" ]; then echo "refusing to publish: Android SDK build-tools directory not found" >&2 exit 1 fi BUILD_TOOLS_DIR=$(find_stable_build_tools_dir "$SDK_ROOT" || true) if [ -z "$BUILD_TOOLS_DIR" ]; then echo "refusing to publish: no versioned build-tools directory has aapt2" >&2 exit 1 fi AAPT2="$BUILD_TOOLS_DIR/aapt2" ROOT_VERSION=$(read_root_semver "$ROOT_DIR/VERSION" || true) if [ -z "$ROOT_VERSION" ]; then echo "refusing to publish: VERSION is missing or not strict semantic version" >&2 exit 1 fi # PB-26-009: Gradle reads release signing passwords during configuration, so # source the existing protected environment before invoking any release task. if [ -f "$HOME/.secrets" ]; then . "$HOME/.secrets" fi "$ROOT_DIR/gradlew" --no-configuration-cache :app:assembleRelease # PB-26-113: the merged release manifest is the shipped permission and exported # component surface. Drift from the checked-in inventory refuses the publish # rather than reaching a store unnoticed. python3 "$ROOT_DIR/scripts/check-release-manifest.py" check || { echo "refusing to publish: release manifest inventory drift" >&2 exit 1 } case "${KEPR_TARGET:-}" in android-arm64) APK_NAME="app-arm64-v8a-release.apk" EXPECTED_ABI="arm64-v8a" ;; android-x86) APK_NAME="app-x86_64-release.apk" EXPECTED_ABI="x86_64" ;; *) echo "refusing to publish: unsupported KEPR_TARGET '${KEPR_TARGET:-unset}'" >&2 exit 1 ;; esac APK="$ROOT_DIR/app/build/outputs/apk/release/$APK_NAME" METADATA="$ROOT_DIR/app/build/outputs/apk/release/output-metadata.json" if [ ! -f "$APK" ] || [ ! -r "$APK" ]; then echo "refusing to publish: release APK was not produced" >&2 exit 1 fi if [ ! -f "$METADATA" ] || [ ! -r "$METADATA" ]; then echo "refusing to publish: Gradle output metadata was not produced" >&2 exit 1 fi # PB-26-123: the inventory is an allow-list of native libraries. It is checked # against the source set AND against the packaged APK, so a library that # arrived only through a dependency is refused too. python3 "$ROOT_DIR/scripts/check-native-provenance.py" "$APK" || { echo "refusing to publish: native provenance drift" >&2 exit 1 } BADGING_FILE=$(mktemp "${TMPDIR:-/tmp}/peach-apk-badging.XXXXXX") cleanup_badging() { [ -e "$BADGING_FILE" ] && trash "$BADGING_FILE" } trap cleanup_badging EXIT HUP INT TERM "$AAPT2" dump badging "$APK" > "$BADGING_FILE" if ! validate_release_metadata \ "$METADATA" \ "$BADGING_FILE" \ "$ROOT_VERSION" \ "com.peachpasswords" \ "$APK_NAME" \ "$EXPECTED_ABI"; then exit 1 fi RELEASE_KEYSTORE=$(sed -n 's/^peachStoreFile=//p' "$ROOT_DIR/local.properties" | sed -n '1p') RELEASE_ALIAS=$(sed -n 's/^peachKeyAlias=//p' "$ROOT_DIR/local.properties" | sed -n '1p') RELEASE_STOREPASS="${PEACH_STORE_PASSWORD:-}" if [ -z "$RELEASE_STOREPASS" ]; then echo "refusing to publish: PEACH_STORE_PASSWORD is not set (source ~/.secrets)" >&2 exit 1 fi if [ -z "$RELEASE_KEYSTORE" ] || [ -z "$RELEASE_ALIAS" ]; then echo "refusing to publish: peachStoreFile or peachKeyAlias missing from local.properties" >&2 exit 1 fi RELEASE_CERTIFICATE=$(release_certificate_sha256 "$RELEASE_KEYSTORE" "$RELEASE_STOREPASS" "$RELEASE_ALIAS") if ! assert_established_release_certificate "$RELEASE_CERTIFICATE"; then exit 1 fi if ! assert_release_signer_matches \ "$RELEASE_CERTIFICATE" \ "$(apk_signer_sha256 "$BUILD_TOOLS_DIR/apksigner" "$APK")" \ "$APK_NAME"; then exit 1 fi # Only a fully verified artifact reaches the Kepr staging directory. The # basename includes SemVer + ABI so Kepr's Content-Disposition carries both # to the browser, matching the {version}/{platform} tokens in kepr.build. mkdir -p "$ROOT_DIR/build/kepr" STAGED_NAME="peach-android-${ROOT_VERSION}-${KEPR_TARGET}.apk" cp "$APK" "$ROOT_DIR/build/kepr/$STAGED_NAME" echo "release APK verified and staged: build/kepr/$STAGED_NAME"