#!/bin/sh # Build a signed release APK. # # Required environment: # KEYSTORE_PATH absolute path to a JKS / PKCS12 keystore # KEYSTORE_PASS password for the keystore # KEY_ALIAS alias of the signing key inside the keystore # KEY_PASS password for that key (often the same as KEYSTORE_PASS) # # Required tooling: # Android build-tools 35.0.0 under ANDROID_SDK_ROOT or on PATH. # # Usage: # scripts/build-apk # # Output: prints the absolute path of the signed APK and its sha256. # The signed APK lives at app/build/outputs/apk/release/app-release.apk. # # This script does NOT generate a keystore. Generate one once with: # keytool -genkey -v -keystore release.jks -alias scrcpy-android \ # -keyalg RSA -keysize 2048 -validity 10000 # and keep it outside the repo (it must never be committed). set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)" APK="$ROOT/app/build/outputs/apk/release/app-release.apk" require() { name="$1" val="$2" if [ -z "$val" ]; then echo "build-apk: \$$name is not set" >&2 echo " see the header of $0 for the required environment" >&2 exit 1 fi } require KEYSTORE_PATH "${KEYSTORE_PATH:-}" require KEYSTORE_PASS "${KEYSTORE_PASS:-}" require KEY_ALIAS "${KEY_ALIAS:-}" require KEY_PASS "${KEY_PASS:-}" if [ ! -f "$KEYSTORE_PATH" ]; then echo "build-apk: keystore not found at $KEYSTORE_PATH" >&2 exit 1 fi "$ROOT/scripts/check-wrapper" "$ROOT/scripts/check-server" echo "build-apk: gradle :app:assembleRelease" cd "$ROOT" ./gradlew --no-daemon :app:assembleRelease if [ ! -f "$APK" ]; then echo "build-apk: gradle finished but $APK does not exist" >&2 exit 1 fi SUM=$(sha256sum "$APK" | awk '{print $1}') APKSIGNER="" AAPT2="" ZIPALIGN="" SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" if [ -n "$SDK_ROOT" ]; then APKSIGNER="$SDK_ROOT/build-tools/35.0.0/apksigner" AAPT2="$SDK_ROOT/build-tools/35.0.0/aapt2" ZIPALIGN="$SDK_ROOT/build-tools/35.0.0/zipalign" [ -x "$APKSIGNER" ] || APKSIGNER="" [ -x "$AAPT2" ] || AAPT2="" [ -x "$ZIPALIGN" ] || ZIPALIGN="" fi if [ -z "$APKSIGNER" ] && command -v apksigner >/dev/null 2>&1; then APKSIGNER=$(command -v apksigner) fi if [ -z "$AAPT2" ] && command -v aapt2 >/dev/null 2>&1; then AAPT2=$(command -v aapt2) fi if [ -z "$ZIPALIGN" ] && command -v zipalign >/dev/null 2>&1; then ZIPALIGN=$(command -v zipalign) fi if [ -z "$APKSIGNER" ] || [ -z "$AAPT2" ] || [ -z "$ZIPALIGN" ]; then echo "build-apk: build-tools 35.0.0 are required" >&2 echo " set ANDROID_SDK_ROOT or put apksigner, aapt2, and zipalign on PATH" >&2 exit 1 fi # Capture rather than pipe: POSIX sh has no pipefail, so # `apksigner verify | sed` reports sed's status and a rejected APK would # sail past set -e and be announced below as if it were signed. echo "build-apk: apksigner verify" if ! verify_out=$("$APKSIGNER" verify --verbose --print-certs "$APK" 2>&1); then printf '%s\n' "$verify_out" >&2 echo "build-apk: apksigner rejected $APK" >&2 exit 1 fi printf '%s\n' "$verify_out" | sed 's/^/ /' "$ZIPALIGN" -c -P 16 4 "$APK" expected_permissions=$(cat <&2 printf '%s\n' "$actual_permissions" >&2 exit 1 fi expected_sdk=$(cat <&2 printf '%s\n' "$actual_sdk" >&2 exit 1 fi expected_abis="native-code: 'arm64-v8a' 'x86_64'" actual_abis=$("$AAPT2" dump badging "$APK" | sed -n '/^native-code:/p') if [ "$actual_abis" != "$expected_abis" ]; then echo "build-apk: unexpected release ABIs: $actual_abis" >&2 exit 1 fi echo "build-apk: $APK" echo "build-apk: sha256 $SUM"