diff --git a/docs/00_项目规范/07_Android粒子Validation_APK构建规范_v1.md b/docs/00_项目规范/07_Android粒子Validation_APK构建规范_v1.md new file mode 100644 index 000000000..10b3b5307 --- /dev/null +++ b/docs/00_项目规范/07_Android粒子Validation_APK构建规范_v1.md @@ -0,0 +1,68 @@ +# Android 粒子 Validation APK 构建规范 v1 + +## 目的 + +固定 `MapLibreAndroidTestApp` 粒子验证 APK 的制作流程,避免每次重新调查 Gradle 参数、Java/Android SDK 环境、native strip 与 APK 体积异常。 + +权威入口: + +```bash +./tools/particle_android_validation/build_validation_apk.sh +``` + +除排错外,不再手工拼接 Validation APK 的 Gradle 命令。 + +## 固定构建契约 + +```text +App: MapLibreAndroidTestApp +Renderer: OpenGL +BuildType: Debug +ABI: x86_64 +Native debug symbols: strip +Gradle console: plain +``` + +核心参数固定为: + +```text +:MapLibreAndroidTestApp:assembleOpenglDebug +-Pmaplibre.abis=x86_64 +-Pmaplibre.validation.stripNativeDebugSymbols=true +--console=plain +``` + +## 必须执行的防错逻辑 + +1. 构建前删除目标 APK 文件,但不做全量 Gradle clean。 +2. Java 必须为 17;Android SDK 必须可解析。 +3. 构建失败时不得复用旧 APK。 +4. 构建成功后必须校验 APK ZIP 完整性。 +5. APK 内 native ABI 必须只有 `x86_64`。 +6. 必须包含 `lib/x86_64/libmaplibre.so`。 +7. 默认 `libmaplibre.so` 不得超过 80 MiB。 +8. 默认 APK 不得超过 100 MiB。 +9. 必须记录 SHA-256、commit、branch、dirty 状态和完整 Gradle 日志。 + +STEP-090/091 的正常 APK 参考体积约 57 MB。数百 MB 通常表示未 strip native debug symbols;约 95 MB 且 ZIP 逻辑内容仍约 60 MB,通常表示同一路径增量打包残留旧 ZIP 尾部数据。 + +## 标准产物目录 + +```text +artifacts/maplibre-particle/android-validation/ +├── MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +├── build-info.txt +└── build.log +``` + +该目录为本地验证产物目录,不进入 Git。 + +## 边界 + +本 Pipeline 只负责构建和 APK 自验,不负责 ADB 安装、Activity 启动、设备交互或 STEP 业务场景验收。 + +Android 上机验证仍由后续独立步骤执行。构建成功不等于 STEP 场景已通过。 + +## 变更规则 + +若未来测试 App、ABI、Renderer、strip 策略或产物门槛发生正式变化,应先修改 Pipeline 脚本与本规范,再使用新流程;不得只在聊天或临时命令中形成新的事实基线。 diff --git a/tools/particle_android_validation/README.md b/tools/particle_android_validation/README.md new file mode 100644 index 000000000..44ae6c0e1 --- /dev/null +++ b/tools/particle_android_validation/README.md @@ -0,0 +1,113 @@ +# Android Particle Validation APK Pipeline + +这是 `MapLibreAndroidTestApp` 粒子验证 APK 的唯一推荐构建入口。 + +以后不要再手工回忆 Gradle 参数,也不要直接拿 `build/outputs` 目录中的旧 APK 做验证。 + +## 一键构建 + +在 Mac 的仓库根目录执行: + +```bash +./tools/particle_android_validation/build_validation_apk.sh +``` + +Pipeline 固定执行: + +```text +MapLibreAndroidTestApp +OpenGL Debug +ABI = x86_64 +stripNativeDebugSymbols = true +``` + +对应 Gradle 核心参数为: + +```text +:MapLibreAndroidTestApp:assembleOpenglDebug +-Pmaplibre.abis=x86_64 +-Pmaplibre.validation.stripNativeDebugSymbols=true +--console=plain +``` + +## 稳定产物 + +成功后统一输出到: + +```text +artifacts/maplibre-particle/android-validation/ +├── MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk +├── build-info.txt +└── build.log +``` + +`artifacts/maplibre-particle/` 已在 `.gitignore` 中,不进入 Git。 + +`build-info.txt` 会记录 commit、branch、dirty 状态、构建时间、ABI、APK 大小、`libmaplibre.so` 大小和 SHA-256。 + +## Pipeline 为什么必须存在 + +历史上有两个容易重复踩中的问题: + +1. 少传 `-Pmaplibre.validation.stripNativeDebugSymbols=true` 时,Debug `libmaplibre.so` 会包含大量 native debug symbols,APK 可膨胀到数百 MB。 +2. 在同一路径先生成超大 APK、再切换 strip 参数增量打包时,APK 文件可能保留旧 ZIP 尾部数据,出现约 95 MB 的物理文件;因此 Pipeline 在每次构建前都会先删除最终 APK,只重新 package,不执行全量 Gradle clean。 + +STEP-090/091 的正常 x86_64 Validation APK 基线约为 57 MB。 + +## 自动环境处理 + +Pipeline 会优先使用已有 `JAVA_HOME` / `ANDROID_HOME`。 + +若未设置,会在当前 Mac 上自动尝试: + +```text +Java 17: + /usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home + /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home + /usr/libexec/java_home -v 17 + +Android SDK: + ~/Library/Android/sdk +``` + +Java 不是 17、Android SDK 不存在或 Gradle 失败时直接停止,不会把旧 APK 当成成功产物。 + +## 构建后自动验收 + +Pipeline 会检查: + +- APK ZIP 完整性; +- native ABI 必须只有 `x86_64`; +- 必须存在 `lib/x86_64/libmaplibre.so`; +- `libmaplibre.so` 默认不得超过 80 MiB; +- APK 默认不得超过 100 MiB; +- 生成 SHA-256; +- 保存完整 Gradle 日志。 + +超过体积门槛默认视为 pipeline 失败,以防再次交付未 strip 或带旧 ZIP 尾部的 APK。 + +如未来 Validation App 因真实资源增长需要调整门槛,可显式覆盖: + +```bash +MAPLIBRE_VALIDATION_MAX_APK_BYTES=134217728 \ +MAPLIBRE_VALIDATION_MAX_LIBMAPLIBRE_BYTES=94371840 \ +./tools/particle_android_validation/build_validation_apk.sh +``` + +也可以修改产物目录: + +```bash +MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \ +./tools/particle_android_validation/build_validation_apk.sh +``` + +## 边界 + +这个 Pipeline 只负责**制作和验收 APK**,不自动执行: + +- ADB 安装; +- 启动 Activity; +- 真机/模拟器交互; +- STEP 场景验收。 + +这样构建产物与上机验证保持分离,避免脚本意外操作设备。 diff --git a/tools/particle_android_validation/build_validation_apk.sh b/tools/particle_android_validation/build_validation_apk.sh new file mode 100755 index 000000000..f1c915560 --- /dev/null +++ b/tools/particle_android_validation/build_validation_apk.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ANDROID_DIR="$REPO_ROOT/platform/android" +SOURCE_APK="$ANDROID_DIR/MapLibreAndroidTestApp/build/outputs/apk/opengl/debug/MapLibreAndroidTestApp-opengl-debug.apk" +OUT_DIR="${MAPLIBRE_VALIDATION_OUT_DIR:-$REPO_ROOT/artifacts/maplibre-particle/android-validation}" +OUT_APK="$OUT_DIR/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk" +OUT_INFO="$OUT_DIR/build-info.txt" +OUT_LOG="$OUT_DIR/build.log" + +MAX_APK_BYTES="${MAPLIBRE_VALIDATION_MAX_APK_BYTES:-104857600}" +MAX_LIB_BYTES="${MAPLIBRE_VALIDATION_MAX_LIBMAPLIBRE_BYTES:-83886080}" + +fail() { + echo "[validation-apk] ERROR: $*" >&2 + exit 1 +} + +resolve_java_home() { + if [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]]; then + return + fi + + local candidate + for candidate in /usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home; do + if [[ -x "$candidate/bin/java" ]]; then + export JAVA_HOME="$candidate" + return + fi + done + + if [[ -x /usr/libexec/java_home ]]; then + candidate="$(/usr/libexec/java_home -v 17 2>/dev/null || true)" + if [[ -n "$candidate" && -x "$candidate/bin/java" ]]; then + export JAVA_HOME="$candidate" + return + fi + fi + + fail "Java 17 not found. Set JAVA_HOME to a Java 17 JDK." +} + +resolve_android_home() { + local candidate="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" + if [[ -n "$candidate" && -d "$candidate" ]]; then + export ANDROID_HOME="$candidate" + export ANDROID_SDK_ROOT="$candidate" + return + fi + + candidate="$HOME/Library/Android/sdk" + if [[ -d "$candidate" ]]; then + export ANDROID_HOME="$candidate" + export ANDROID_SDK_ROOT="$candidate" + return + fi + + fail "Android SDK not found. Set ANDROID_HOME or ANDROID_SDK_ROOT." +} + +resolve_java_home +resolve_android_home +export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH" + +JAVA_VERSION="$("$JAVA_HOME/bin/java" -version 2>&1 | head -n 1)" +[[ "$JAVA_VERSION" == *'"17.'* ]] || fail "Java 17 required; got: $JAVA_VERSION" +[[ -x "$ANDROID_DIR/gradlew" ]] || fail "Gradle wrapper not found: $ANDROID_DIR/gradlew" + +mkdir -p "$OUT_DIR" +rm -f "$SOURCE_APK" "$OUT_APK" + +COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)" +BRANCH="$(git -C "$REPO_ROOT" branch --show-current)" +DIRTY_COUNT="$(git -C "$REPO_ROOT" status --porcelain | wc -l | tr -d ' ')" + +echo "[validation-apk] commit=$COMMIT branch=$BRANCH" +echo "[validation-apk] java=$JAVA_VERSION" +echo "[validation-apk] android_sdk=$ANDROID_HOME" +echo "[validation-apk] output=$OUT_APK" + +set +e +( + cd "$ANDROID_DIR" + ./gradlew :MapLibreAndroidTestApp:assembleOpenglDebug -Pmaplibre.abis=x86_64 -Pmaplibre.validation.stripNativeDebugSymbols=true --console=plain +) >"$OUT_LOG" 2>&1 +GRADLE_RC=$? +set -e + +if [[ "$GRADLE_RC" -ne 0 ]]; then + tail -n 120 "$OUT_LOG" >&2 || true + fail "Gradle build failed with exit code $GRADLE_RC. Full log: $OUT_LOG" +fi + +[[ -f "$SOURCE_APK" ]] || fail "Build succeeded but APK was not produced." +unzip -tq "$SOURCE_APK" >/dev/null || fail "APK ZIP integrity check failed." + +ABI_LIST="$(unzip -Z1 "$SOURCE_APK" | sed -n 's#^lib/\([^/]*\)/.*#\1#p' | sort -u | tr '\n' ' ' | sed 's/ $//')" +[[ "$ABI_LIST" == "x86_64" ]] || fail "Unexpected APK ABI set: [$ABI_LIST]" + +LIB_BYTES="$(unzip -l "$SOURCE_APK" 'lib/x86_64/libmaplibre.so' | awk '/lib\/x86_64\/libmaplibre\.so$/ {print $1; exit}')" +[[ -n "$LIB_BYTES" ]] || fail "lib/x86_64/libmaplibre.so not found in APK." + +APK_BYTES="$(wc -c < "$SOURCE_APK" | tr -d ' ')" +if (( LIB_BYTES > MAX_LIB_BYTES )); then + fail "libmaplibre.so is too large ($LIB_BYTES bytes). Native debug symbols may not be stripped." +fi +if (( APK_BYTES > MAX_APK_BYTES )); then + fail "APK is too large ($APK_BYTES bytes). Refuse to publish a likely stale/unstripped package." +fi + +cp -f "$SOURCE_APK" "$OUT_APK" +SHA256="$(shasum -a 256 "$OUT_APK" | awk '{print $1}')" +BUILT_AT_UTC="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +GIT_DIRTY="false" +[[ "$DIRTY_COUNT" -gt 0 ]] && GIT_DIRTY="true" + +cat >"$OUT_INFO" <