Files
opendance-android/build/verify.sh
T
zijunchiang 95429e8ce3 test(native): APK native 库体检——ELF 类型 / DT_NEEDED 闭包 / 16 KB 页对齐(T-AND-015)
T-AND-012 的结论是「ExceptionInInitializerError 的根因在集成配置(native 解压 +
R8 + ABI),不是 MediaPipe 本身不稳定」,但当时的核查是**手工一次性**的(unzip 看
lib/、grep seeds、strings 找类名)——下次照样可能漏,且漏了也没人发现,只会在用户
装上手机、点开相机那一刻暴露。

新增 build/check_native_libs.py(纯 Python 标准库解析 ELF,不需要 readelf / NDK),
检查三项**无设备也能查**的「一装上就崩」根因:
  * ABI 与 ELF 类型匹配(把 x86_64 的 .so 放进 lib/arm64-v8a/ → 一加载就崩);
  * DT_NEEDED 依赖闭包(非系统库必须同 ABI 目录内自带;MediaPipe 早期版本的
    libc++_shared.so 就是这么缺的);
  * PT_LOAD 页对齐 ≥ 16 KB(Android 15 起部分设备页 16 KB,4 KB 对齐会直接加载失败;
    Google Play 2025-11 起也要求)。
外加 zipalign -c -P 16 -v 4(有 build-tools 时)。已接入 verify.sh 与 accept-release.sh。

实测(已发布的 opendance-android-v0.2.0.zip 内 APK):
  双 ABI 下 6 个 .so 全部 ELF 类型匹配、依赖闭包完整(libmediapipe_tasks_jni.so 只依赖
  libdl/libjnigraphics/libEGL/libGLESv2/libm/libz/liblog/libandroid/libc,全是系统库)、
  PT_LOAD p_align=0x4000(16 KB 兼容)、zipalign -P 16 通过。
  结论:打包配置这一侧的「一装上就崩」风险已排除,剩下的只能靠真机实测。

与 readelf 交叉验证(6 个 .so 的 DT_NEEDED / p_align / e_machine 逐项一致)——
这次交叉验证抓到解析器自己的 bug:第一版程序头偏移写错(ELF64 的
p_offset/p_vaddr 之后第三个 QWORD 是 p_paddr 而非 p_filesz),导致 vaddr→offset
映射失败、DT_NEEDED 解析为空,页面照样打印「✓ 依赖闭包完整(系统库 0 项)」——
一句**假通过**,唯一破绽就是那个 0。教训:通过输出必须带可核对的数量/取值。

反向验证(三条分支都用真实负例证明会失败):
  * 注入宿主 x86_64 的 libz.so.1 到 lib/arm64-v8a/libbogus.so → 同时报 ELF 类型不符、
    缺依赖 libc.so.6、页对齐 0x1000 < 16384;
  * 把 libz.so 从系统库白名单去掉 → 报缺少依赖 libz.so(证明闭包分支是活的);
  * --expected-abis 传错 → 报 ABI 集合不符。

verify.sh 全绿;对**已发布资产**重新下载后跑完整 accept-release.sh 全部通过。
未新发版本:APK 本身未变(本任务是检查工具与文档),README 的新增项随下次发布进包。
2026-09-17 05:25:04 +08:00

174 lines
6.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================
# OpenDance Studio —— Android 辅助 App 一键验证
#
# 依次执行:单元测试 → debug 构建 → release 构建 → 产物校验
# 任一环节失败即退出非 0。
#
# 用法:
# JAVA_HOME=/path/to/jdk17 ANDROID_HOME=/path/to/sdk bash build/verify.sh
# ============================================================
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
FAILED=0
echo "============================================================"
echo " 1/3 单元测试(JVM,无需设备)"
echo "============================================================"
if ./gradlew testDebugUnitTest --console=plain -q 2>&1 | tail -20; then
echo "✓ 单元测试通过"
else
echo "✗ 单元测试失败"
FAILED=1
fi
echo " 测试报告:app/build/reports/tests/testDebugUnitTest/index.html"
echo ""
echo "============================================================"
echo " 2/3 构建 debug + release APK"
echo "============================================================"
if bash build/build-apk.sh all 2>&1 | grep -vE "^> Task|^$" | tail -20; then
:
else
FAILED=1
fi
echo ""
echo "============================================================"
echo " 3/3 产物校验(签名 / 版本元数据 / 包信息 / 模型资源 / ABI / R8)"
echo "============================================================"
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
# 优先选用与 compileSdk 匹配的 build-tools,缺失时退回最新版
if [ -d "$SDK/build-tools/34.0.0" ]; then
BT="$SDK/build-tools/34.0.0"
else
BT="$(ls -d "$SDK"/build-tools/* 2>/dev/null | sort -V | tail -1)"
fi
RELEASE_APK="app/build/outputs/apk/release/app-release.apk"
if [ ! -f "$RELEASE_APK" ]; then
echo "✗ 未生成 $RELEASE_APK"
FAILED=1
elif [ -z "$BT" ]; then
echo "✗ 未找到 build-tools,无法校验"
FAILED=1
else
# 以**退出码**为准:不同 build-tools 版本的 apksigner 输出文案不一致
# (34.0.0 会打印 "Verifies",36.0.0 配 --print-certs 时只打印证书信息),
# 依赖日志字符串判断会导致误判。
"$BT/apksigner" verify "$RELEASE_APK" > /tmp/apk_verify.txt 2>&1
VERIFY_STATUS=$?
"$BT/apksigner" verify --print-certs "$RELEASE_APK" > /tmp/apk_sign.txt 2>/dev/null
if [ "$VERIFY_STATUS" -eq 0 ]; then
echo "✓ 签名校验通过(apksigner 退出码 0)"
grep -E "Signer #1 certificate DN" /tmp/apk_sign.txt | sed 's/^/ /' || true
if grep -q "APK Signature Scheme v2" /tmp/apk_verify.txt 2>/dev/null; then
echo " v2 签名方案:已启用"
fi
else
echo "✗ 签名校验失败(apksigner 退出码 $VERIFY_STATUS)"
cat /tmp/apk_verify.txt
FAILED=1
fi
"$BT/aapt2" dump badging "$RELEASE_APK" 2>/dev/null | head -1 | sed 's/^/ /'
# ---- 版本元数据必须落在**产物本身**上(文档 §11.2)----
# gradle 的 verifyReleaseVersion 只比构建配置;APK 的 AndroidManifest 才是
# 安装界面 / 应用信息页读到的版本。构建缓存或拷错文件都只会在这一层暴露。
if bash "$PROJECT_DIR/build/check-apk-version.sh" "$RELEASE_APK"; then
:
else
echo "✗ APK 的 versionName/versionCode 与 project.yaml 不一致"
FAILED=1
fi
if unzip -l "$RELEASE_APK" | grep -q "assets/pose_landmarker_lite.task"; then
echo "✓ MediaPipe 模型已随包分发"
else
echo "✗ APK 中缺少 MediaPipe 模型"
FAILED=1
fi
# ---- T-AND-012:ABI 必须恰为真机 ABI,且每个 ABI 下都有 MediaPipe native 库 ----
ABIS=$(unzip -l "$RELEASE_APK" | grep -oE "lib/[^/]+/" | sed 's#lib/##;s#/##' | sort -u)
ABIS_ONE_LINE=$(echo "$ABIS" | tr '\n' ' ')
echo " 包含 ABI:$ABIS_ONE_LINE"
EXPECTED_ABIS=$(printf 'arm64-v8a\narmeabi-v7a')
if [ "$ABIS" = "$EXPECTED_ABIS" ]; then
echo "✓ release ABI 恰为 arm64-v8a + armeabi-v7a(真机 ABI)"
else
echo "✗ release ABI 与预期不符(预期 arm64-v8a / armeabi-v7a)"
FAILED=1
fi
if echo "$ABIS" | grep -qE "x86"; then
echo "✗ release APK 含模拟器 ABI,会导致包体翻倍且与 abiFilters 不符"
FAILED=1
fi
for abi in arm64-v8a armeabi-v7a; do
if unzip -l "$RELEASE_APK" | grep -q "lib/$abi/.*mediapipe"; then
echo "✓ lib/$abi 下有 MediaPipe native 库"
else
echo "✗ lib/$abi 下缺少 MediaPipe native 库(真机安装后必然初始化失败)"
FAILED=1
fi
done
# ---- T-AND-015:native 库体检(ABI / ELF 类型 / 依赖闭包 / 16 KB 页对齐)----
NPY="${PYTHON_BIN:-python3}"
if command -v "$NPY" >/dev/null 2>&1; then
# 期望 ABI 用上面已经算好的 EXPECTED_ABIS(项目声明的真机 ABI),
# 而不是 APK 自己的 ABI 列表 —— 否则「ABI 是否符合预期」这条就自我循环了
if "$NPY" "$PROJECT_DIR/build/check_native_libs.py" "$RELEASE_APK" \
--expected-abis "$(echo "$EXPECTED_ABIS" | tr '\n' ',' | sed 's/,$//')" \
>/tmp/apk_native.txt 2>&1; then
grep -E "✓ native 库体检通过" /tmp/apk_native.txt | sed 's/^/ /'
else
echo "✗ native 库体检未通过:"
sed 's/^/ /' /tmp/apk_native.txt | tail -20
FAILED=1
fi
else
echo "✗ 未找到 $NPY,无法做 native 库体检"
FAILED=1
fi
# ---- T-AND-012:R8 不得裁掉 MediaPipe(seeds 由 -printseeds 输出) ----
SEEDS="app/build/outputs/mapping/release/seeds.txt"
if [ -f "$SEEDS" ]; then
MP_SEEDS=$(grep -c "^com.google.mediapipe" "$SEEDS" || true)
if [ "$MP_SEEDS" -gt 0 ]; then
echo "✓ R8 保留 MediaPipe 类 $MP_SEEDS 条(seeds.txt)"
else
echo "✗ R8 把 MediaPipe 整体裁掉了,release 包必然崩溃"
FAILED=1
fi
else
echo "✗ 未生成 $SEEDS(proguard-rules.pro 的 -printseeds 是否被移除?)"
FAILED=1
fi
# ---- T-AND-012:关键类必须真的在 dex 里 ----
if unzip -p "$RELEASE_APK" classes.dex 2>/dev/null | grep -qa "poselandmarker/PoseLandmarker"; then
echo "✓ dex 中存在 PoseLandmarker 类"
else
echo "✗ dex 中找不到 PoseLandmarker(R8/打包异常)"
FAILED=1
fi
fi
echo ""
if [ "$FAILED" -ne 0 ]; then
echo "============================================================"
echo " 验证失败"
echo "============================================================"
exit 1
fi
echo "============================================================"
echo " 验证全部通过"
echo "============================================================"