#!/usr/bin/env bash # compile-android.sh — build+embed libbox.aar, build a signed Flutter # Android APK, package into dist/. # # Mirrors ~/code/jiu/scripts/ci/compile-android.sh, adapted for pangolin: # - version derived via pangolin's ver_from_tag (client-v* tag; _env.sh), # not jiu's ad-hoc `${TAG#client-v}` strip. # - pangolin embeds sing-box as a native library (io.nekohasekai.libbox, # gomobile bind) — jiu's Flutter app has no embedded core at all. This # script MUST build+deploy libbox.aar (scripts/build-libbox.sh android) # before `flutter build apk`, or the APK links no VPN kernel at all. # Needs JDK 17 (gomobile bind javac step) — see CLAUDE.md "client/ 移动端" # and "已知坑" for why: JDK 21 is rejected outright, NDK 27 fails link. # - signing secrets: RELEASE_KEYSTORE (base64) + KEY_PASSWORD are the # secret names given for this task ("actual configured" names). jiu uses # 4 separate secrets (ANDROID_KEYSTORE_BASE64/ANDROID_KEYSTORE_PASSWORD/ # ANDROID_KEY_ALIAS/ANDROID_KEY_PASSWORD), and pangolin's OWN prior CI # design docs (docs/cicd-design.html, docs/superpowers/plans/2026-07-05- # cicd.md, docs/superpowers/specs/2026-07-05-cicd-design.md) planned the # same 4-secret jiu-style naming. That is a real conflict with this # task's brief — see the TODO(controller) block below and the draft # report. This script follows the 2-secret brief as instructed, assuming # storePassword == keyPassword == KEY_PASSWORD and a derivable keyAlias # (default "pangolin", overridable via optional RELEASE_KEY_ALIAS). # - dart-define is PANGOLIN_API_URL (client/lib/services/api_config.dart), # not jiu's BASE_URL/PUBLIC_URL/APP_VERSION trio. # - build.gradle previously signed release builds with signingConfigs.debug # (no release signing config existed at all) — this task's draft also # wires client/android/app/build.gradle to add a `release` signingConfig # that reads client/android/key.properties (falls back to debug when # key.properties is absent, so local/no-secret builds still work). See # report for the exact diff. # - universal APK (`flutter build apk --release`, no --split-per-abi) — see # report for the size-vs-single-stable-URL tradeoff discussion; this # deviates from docs/superpowers/plans/2026-07-05-cicd.md Task 7 which # had planned --split-per-abi. # - output dist/pangolin-android.apk (stable name; deploy-client.sh keeps # only the latest one under this name on pangolin1). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=scripts/ci/_env.sh . "${SCRIPT_DIR}/_env.sh" TAG="${1:?usage: compile-android.sh }" # No $() anywhere (repo convention): ver_from_tag prints to stdout, captured # via a temp file + `read`, same pattern as release-server.sh. ver_file="/tmp/compile_android_ver.$$" ver_from_tag client "$TAG" > "$ver_file" VER="" # `|| true`: ver_from_tag uses printf '%s' (no trailing newline); `read` # hitting EOF without a newline returns 1 even though VER was assigned — # under `set -e` that would abort here. Same rationale as release-server.sh. read -r VER < "$ver_file" || true rm -f "$ver_file" # versionCode 必须单调递增,否则 Android 无法覆盖升级安装(同 jiu 注释)。 # 由版本号推导:major*10000 + minor*100 + patch(如 1.0.48 -> 10048)。 # Pure parameter expansion (no `cut`/`$()`), and strip any `-suffix` off the # patch component so the arithmetic below stays numeric (e.g. "48-rc1"->48). MAJOR="${VER%%.*}" _REST="${VER#*.}" MINOR="${_REST%%.*}" PATCH="${_REST#*.}" PATCH="${PATCH%%-*}" BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH )) echo "==> compile-android: tag=${TAG} version=${VER} build=${BUILD}" API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}" # ── [1/3] build+embed libbox.aar (io.nekohasekai.libbox) ──────────────────── # Must run BEFORE `flutter build apk` — build.gradle's dependency block reads # app/kernel/dist/android/libbox.aar and just logs a warning (does not fail # the build!) if it's missing, which would silently ship an APK with no VPN # kernel. Fail loudly here instead. JAVA_HOME_17="${JAVA_HOME_17:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}" if [ ! -d "$JAVA_HOME_17" ]; then echo "ERROR: JDK 17 not found at ${JAVA_HOME_17} (required for gomobile Android bind)." >&2 echo " See CLAUDE.md 'libbox 构建' — install via 'brew install openjdk@17' or set JAVA_HOME_17." >&2 exit 1 fi echo "==> compile-android: building libbox.aar (JAVA_HOME=${JAVA_HOME_17}, NDK auto-detected >=28)" JAVA_HOME="$JAVA_HOME_17" bash scripts/build-libbox.sh android LIBBOX_AAR="app/kernel/dist/android/libbox.aar" if [ ! -f "$LIBBOX_AAR" ]; then echo "ERROR: ${LIBBOX_AAR} not found after build-libbox.sh — aborting." >&2 exit 1 fi # ── [2/3] sync pubspec version + release signing ──────────────────────────── # BSD sed (macOS runner — mac-pangolin-2, see docs/ci-runner.md), same as jiu. sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml # 必须 release 签名。缺任一签名 secret 直接失败,绝不回退 debug:debug 包用公开 # 调试密钥签名,不能正式分发,也无法与正式版互相覆盖升级(同 jiu 注释)。 : "${RELEASE_KEYSTORE:?缺少 RELEASE_KEYSTORE(base64 编码的 release keystore):未配置签名,构建中止(不回退 debug)}" : "${KEY_PASSWORD:?缺少 KEY_PASSWORD:未配置签名,构建中止}" # TODO(controller): confirm this 2-secret assumption before relying on it. # - Assumes storePassword == keyPassword == KEY_PASSWORD (true only if the # keystore was generated with the same password for both). # - keyAlias is NOT among the secrets this task named, so it defaults to # "pangolin" below — override with RELEASE_KEY_ALIAS if the real alias # differs (e.g. keytool default "androiddebugkey" is NOT this — that # alias belongs to the debug keystore, never use it for release signing). # - pangolin's OWN prior CI design docs (docs/cicd-design.html §secrets, # docs/superpowers/plans/2026-07-05-cicd.md Task 6/7, # docs/superpowers/specs/2026-07-05-cicd-design.md) planned 4 separate # secrets instead: ANDROID_KEYSTORE_BASE64 / ANDROID_KEYSTORE_PASSWORD / # ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD. If THAT naming is what is # actually configured in Forgejo (not RELEASE_KEYSTORE/KEY_PASSWORD), # this script needs updating before first real run. RELEASE_KEY_ALIAS="${RELEASE_KEY_ALIAS:-pangolin}" echo "==> compile-android: configuring release signing (keyAlias=${RELEASE_KEY_ALIAS})" KEYSTORE_PATH="${PWD}/client/android/pangolin-release.jks" printf '%s' "$RELEASE_KEYSTORE" | base64 --decode > "$KEYSTORE_PATH" cat > client/android/key.properties <&2 exit 1 fi cp "$APK" dist/pangolin-android.apk # Clean up signing material so it never lingers on the runner workspace. rm -f client/android/key.properties "$KEYSTORE_PATH" 2>/dev/null || true echo "==> compile-android: done — dist/ contents:" ls -lh dist/