162 lines
6.7 KiB
Shell
Executable file
162 lines
6.7 KiB
Shell
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
ADB="${ADB:-adb}"
|
|
PACKAGE="io.github.zapretkvn.android"
|
|
ACTIVITY="$PACKAGE/.MainActivity"
|
|
REPORT_DIR="${ZAPRET_RC_REPORT_DIR:-$PROJECT_ROOT/build/release-candidate}"
|
|
TEMP_DIR="$(mktemp -d -t zapret-kvn-release-device-XXXXXXXX)"
|
|
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$PROJECT_ROOT/core.properties"
|
|
if [[ -z "${ANDROID_HOME:-}" && -f "$PROJECT_ROOT/local.properties" ]]; then
|
|
ANDROID_HOME="$(sed -n 's/^sdk\.dir=//p' "$PROJECT_ROOT/local.properties" | tail -n 1)"
|
|
export ANDROID_HOME
|
|
fi
|
|
: "${ANDROID_HOME:?ANDROID_HOME must point to an installed Android SDK}"
|
|
APKSIGNER="$ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS/apksigner"
|
|
|
|
cleanup() {
|
|
"$ADB" uninstall "$PACKAGE" >/dev/null 2>&1 || true
|
|
rm -rf -- "$TEMP_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
for command in "$ADB" awk jq keytool sed sort; do
|
|
command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
|
|
done
|
|
[[ -x "$APKSIGNER" ]]
|
|
|
|
device_count="$($ADB devices | awk 'NR > 1 && $2 == "device" { count++ } END { print count+0 }')"
|
|
if [[ "$device_count" -ne 1 ]]; then
|
|
echo "Expected exactly one ready adb device, found $device_count" >&2
|
|
exit 1
|
|
fi
|
|
|
|
device_abi="$($ADB shell getprop ro.product.cpu.abi | tr -d '\r')"
|
|
case "$device_abi" in
|
|
arm64-v8a) release_abi=arm64-v8a ;;
|
|
armeabi-v7a) release_abi=armeabi-v7a ;;
|
|
x86_64) release_abi=x86_64 ;;
|
|
*) echo "Unsupported release-candidate device ABI: $device_abi" >&2; exit 1 ;;
|
|
esac
|
|
device_sdk="$($ADB shell getprop ro.build.version.sdk | tr -d '\r')"
|
|
device_model="$($ADB shell getprop ro.product.model | tr -d '\r')"
|
|
|
|
keytool -genkeypair -noprompt \
|
|
-keystore "$TEMP_DIR/release-test.jks" -storepass changeit -keypass changeit \
|
|
-alias release -keyalg RSA -keysize 3072 -validity 30 \
|
|
-dname "CN=Zapret KVN release-device test" >/dev/null 2>&1
|
|
keytool -genkeypair -noprompt \
|
|
-keystore "$TEMP_DIR/other.jks" -storepass changeit -keypass changeit \
|
|
-alias other -keyalg RSA -keysize 3072 -validity 30 \
|
|
-dname "CN=Zapret KVN incompatible test" >/dev/null 2>&1
|
|
|
|
build_release() {
|
|
local version_name="$1"
|
|
local version_code="$2"
|
|
local output="$3"
|
|
env \
|
|
ZAPRET_SIGNING_STORE_FILE="$TEMP_DIR/release-test.jks" \
|
|
ZAPRET_SIGNING_STORE_PASSWORD=changeit \
|
|
ZAPRET_SIGNING_KEY_ALIAS=release \
|
|
ZAPRET_SIGNING_KEY_PASSWORD=changeit \
|
|
"$PROJECT_ROOT/gradlew" --no-configuration-cache :app:assembleRelease \
|
|
-PzapretAbi="$release_abi" \
|
|
-PzapretVersionName="$version_name" \
|
|
-PzapretVersionCode="$version_code" >/dev/null
|
|
mapfile -t apks < <(find "$PROJECT_ROOT/app/build/outputs/apk/release" -maxdepth 1 -type f -name '*.apk' | sort)
|
|
if [[ "${#apks[@]}" -ne 1 ]]; then
|
|
echo "Expected one signed release APK, found ${#apks[@]}" >&2
|
|
exit 1
|
|
fi
|
|
"$APKSIGNER" verify --verbose --print-certs "${apks[0]}" >/dev/null
|
|
cp "${apks[0]}" "$output"
|
|
}
|
|
|
|
cd "$PROJECT_ROOT"
|
|
build_release 0.8.0-rc-device1 800001 "$TEMP_DIR/v1.apk"
|
|
build_release 0.8.0-rc-device2 800002 "$TEMP_DIR/v2.apk"
|
|
cert_v1="$($APKSIGNER verify --print-certs "$TEMP_DIR/v1.apk" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')"
|
|
cert_v2="$($APKSIGNER verify --print-certs "$TEMP_DIR/v2.apk" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')"
|
|
[[ -n "$cert_v1" && "$cert_v1" == "$cert_v2" ]]
|
|
|
|
package_version() {
|
|
"$ADB" shell dumpsys package "$PACKAGE" \
|
|
| sed -n 's/^[[:space:]]*versionCode=\([0-9]*\).*/\1/p' \
|
|
| head -n 1 \
|
|
| tr -d '\r'
|
|
}
|
|
|
|
"$ADB" uninstall "$PACKAGE" >/dev/null 2>&1 || true
|
|
"$ADB" logcat -c
|
|
"$ADB" install "$TEMP_DIR/v1.apk" >/dev/null
|
|
[[ "$(package_version)" == 800001 ]]
|
|
if "$ADB" shell run-as "$PACKAGE" id >/dev/null 2>&1; then
|
|
echo "Release application is unexpectedly debuggable" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$REPORT_DIR"
|
|
: > "$REPORT_DIR/cold-start.jsonl"
|
|
for run in 1 2 3 4 5; do
|
|
launch="$($ADB shell am start -S -W -n "$ACTIVITY" | tr -d '\r')"
|
|
total_time="$(sed -n 's/^TotalTime: //p' <<<"$launch" | tail -n 1)"
|
|
wait_time="$(sed -n 's/^WaitTime: //p' <<<"$launch" | tail -n 1)"
|
|
[[ "$total_time" =~ ^[0-9]+$ && "$wait_time" =~ ^[0-9]+$ ]]
|
|
"$ADB" shell pidof "$PACKAGE" >/dev/null
|
|
jq -cn \
|
|
--argjson run "$run" \
|
|
--argjson total_time_ms "$total_time" \
|
|
--argjson wait_time_ms "$wait_time" \
|
|
'{run:$run,total_time_ms:$total_time_ms,wait_time_ms:$wait_time_ms}' \
|
|
>> "$REPORT_DIR/cold-start.jsonl"
|
|
done
|
|
|
|
mapfile -t times < <(jq -r '.total_time_ms' "$REPORT_DIR/cold-start.jsonl" | sort -n)
|
|
median_cold_start_ms="${times[2]}"
|
|
if (( median_cold_start_ms > 5000 )); then
|
|
echo "Release cold-start median exceeds 5000 ms: $median_cold_start_ms" >&2
|
|
exit 1
|
|
fi
|
|
if "$ADB" logcat -d -b crash | grep -Fq "$PACKAGE"; then
|
|
echo "Release process crashed during cold-start gate" >&2
|
|
"$ADB" logcat -d -b crash >&2
|
|
exit 1
|
|
fi
|
|
|
|
first_install_time="$($ADB shell dumpsys package "$PACKAGE" | sed -n 's/^[[:space:]]*firstInstallTime=//p' | head -n 1 | tr -d '\r')"
|
|
"$ADB" install -r "$TEMP_DIR/v2.apk" >/dev/null
|
|
[[ "$(package_version)" == 800002 ]]
|
|
[[ "$($ADB shell dumpsys package "$PACKAGE" | sed -n 's/^[[:space:]]*firstInstallTime=//p' | head -n 1 | tr -d '\r')" == "$first_install_time" ]]
|
|
"$ADB" shell am start -S -W -n "$ACTIVITY" >/dev/null
|
|
"$ADB" shell pidof "$PACKAGE" >/dev/null
|
|
|
|
if downgrade_output="$($ADB install -r "$TEMP_DIR/v1.apk" 2>&1)"; then
|
|
echo "Android unexpectedly accepted a release versionCode downgrade" >&2
|
|
exit 1
|
|
fi
|
|
[[ "$downgrade_output" == *"VERSION_DOWNGRADE"* ]]
|
|
|
|
"$APKSIGNER" sign \
|
|
--ks "$TEMP_DIR/other.jks" --ks-pass pass:changeit --key-pass pass:changeit \
|
|
--out "$TEMP_DIR/other-signed.apk" "$TEMP_DIR/v2.apk"
|
|
if signature_output="$($ADB install -r "$TEMP_DIR/other-signed.apk" 2>&1)"; then
|
|
echo "Android unexpectedly accepted an incompatible release signing key" >&2
|
|
exit 1
|
|
fi
|
|
[[ "$signature_output" == *"UPDATE_INCOMPATIBLE"* ]]
|
|
[[ "$(package_version)" == 800002 ]]
|
|
|
|
jq -n \
|
|
--arg device_model "$device_model" \
|
|
--argjson api "$device_sdk" \
|
|
--arg abi "$device_abi" \
|
|
--argjson cold_start_runs 5 \
|
|
--argjson median_cold_start_ms "$median_cold_start_ms" \
|
|
--arg signing_identity "ephemeral-test-key" \
|
|
'{device_model:$device_model,api:$api,abi:$abi,cold_start_runs:$cold_start_runs,median_cold_start_ms:$median_cold_start_ms,clean_install:true,same_key_upgrade:true,downgrade_rejected:true,incompatible_signature_rejected:true,release_debuggable:false,process_crash:false,signing_identity:$signing_identity}' \
|
|
> "$REPORT_DIR/device-report.json"
|
|
|
|
echo "Release device gate passed: $REPORT_DIR/device-report.json"
|