Files
opendance-android/build/accept-release.sh
T
zijunchiang c207e7dadd test(accept): 发布包级端到端验收(读 zip 内 APK)+ T-AND-014
`build/verify.sh` 验的是 app/build/outputs/ 里**刚构建**的 APK,而用户装的是 zip
里的 app-release.apk。两端都因此翻过车(Web v0.7.4 实体注册、Windows v0.2.1 的
PE 版本元数据),故三端统一:每次发布都要有一条读产物本身的验收(文档 §7.5)。

新增 build/accept-release.sh,对一个 zip(或已解压目录)跑完:
  1) sha256(有 SHA256SUMS 时);
  2) 解压 + 文件清单(apk / README.txt / THIRD_PARTY.md / contracts/VERSION);
  3) APK 的 AndroidManifest versionName/versionCode = 发布版本(复用
     check-apk-version.sh —— 安装界面与应用信息页读的就是它);
  4) ABI 恰为 arm64-v8a + armeabi-v7a、无模拟器 ABI,且两个 ABI 下都有
     MediaPipe native 库(T-AND-012 崩溃根因之一);
  5) assets/pose_landmarker_lite.task 在包内;dex 里仍有
     poselandmarker/PoseLandmarker(R8 没整体裁掉);
  6) 有 apksigner 时顺带验签;
  7) 包内 contracts/VERSION 与仓库逐项一致(抓「APK 用旧契约构建、快照拷了新的」)。
package-release.sh 结尾打印下一步验收命令;README 补用法。

实测(对**已发布**的 v0.2.0 资产重新下载后跑):sha256 OK、版本元数据 0.2.0/200、
ABI 恰为两个真机 ABI、两个 ABI 下都有 MediaPipe native、模型资源在、dex 命中
PoseLandmarker、apksigner 通过、契约快照与仓库一致 → 发布包级验收全部通过。

反向验证(真实负例):
  * 篡改包内 contracts/VERSION 的 data-models=0.6.0 →
    ✗ data-models:包内 0.6.0,仓库 0.7.0,退出码 1;
  * 期望版本写 0.2.1 对 v0.2.0 的包 → ✗ versionName 是 '0.2.0',期望 '0.2.1'
    且 ✗ versionCode 是 200,期望 201。

诚实声明:**装到真机跑 30 分钟**这条脚本替代不了,仍属设备实测项;本脚本能证明的是
「装得上、不会因打包配置而崩」。verify.sh 仍全绿。
2026-09-17 04:51:16 +08:00

190 lines
7.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.
#!/usr/bin/env bash
# ============================================================
# OpenDance Studio Android 辅助 App —— **发布包级端到端验收**(T-AND-014)
#
# 为什么需要它:`build/verify.sh` 验的是 `app/build/outputs/` 里**刚构建**的 APK,
# 而交付给用户的是 zip 里的 `app-release.apk`。两者之间隔着打包与分发;
# 装到手机上的是后者。本项目在另两端都因此翻过车(Web v0.7.4 的实体注册、
# Windows v0.2.1 的 PE 版本元数据),所以三端统一要求:
# **每次发布都要有一条读产物本身的验收**(文档 §7.5)。
#
# 本脚本对一个 zip(或已解压目录)跑完:
# 1. sha256 校验(有 SHA256SUMS 时);
# 2. 解压 + 文件清单(apk / README / THIRD_PARTY / contracts);
# 3. APK 的 **AndroidManifest versionName/versionCode** = 本次发布版本
# (build/check-apk-version.sh;安装界面与应用信息页读的就是它);
# 4. ABI 恰为 arm64-v8a + armeabi-v7a(无模拟器 ABI),且两个 ABI 下都有
# MediaPipe native 库(T-AND-012 的崩溃根因之一);
# 5. MediaPipe 模型资源随包分发、dex 里仍有 PoseLandmarker(R8 没裁掉);
# 6. 有 apksigner 时顺带验签(可选,不影响结论);
# 7. 包内 contracts/VERSION 与仓库 contracts/VERSION 逐项一致
# (抓「APK 用旧契约构建、快照却拷了新的」)。
#
# 用法:
# bash build/accept-release.sh dist-release/opendance-android-v0.2.0.zip
# bash build/accept-release.sh <zip 或已解压目录> 0.2.0 # 显式期望版本
#
# 可选环境变量:
# ANDROID_HOME / ANDROID_SDK_ROOT 找 apksigner 用(找不到就跳过验签)
# WORK_DIR 解压目录(默认 mktemp -d)
# ============================================================
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$PROJECT_DIR"
TARGET="${1:-}"
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
EXPECTED="${2:-$YAML_VERSION}"
WORK_DIR="${WORK_DIR:-$(mktemp -d)}"
FAILED=0
log() { printf '\033[1;34m[accept]\033[0m %s\n' "$*"; }
fail() { printf '\033[1;31m[accept] ✗ %s\033[0m\n' "$*" >&2; FAILED=1; }
if [ -z "$TARGET" ] || [ ! -e "$TARGET" ]; then
echo "用法:bash build/accept-release.sh <发布包 zip 或已解压目录> [期望版本]" >&2
exit 2
fi
if [ -z "$EXPECTED" ]; then
echo "✗ 无法确定期望版本(project.yaml 里没有 version,也没有显式传入)" >&2
exit 2
fi
log "目标:$TARGET"
log "期望版本:$EXPECTED"
log "工作目录:$WORK_DIR"
mkdir -p "$WORK_DIR"
# ---- 1. sha256 + 解压 ----
if [ -f "$TARGET" ]; then
# SHA256SUMS 里是裸文件名,校验时 cwd 必须切到包所在目录;sums 文件用绝对路径
TARGET_DIR_ABS="$(cd "$(dirname "$TARGET")" && pwd)"
SUMS="$TARGET_DIR_ABS/SHA256SUMS"
if [ -f "$SUMS" ]; then
log "sha256 校验($SUMS):"
( cd "$TARGET_DIR_ABS" && sha256sum -c "$SUMS" --ignore-missing ) || fail "sha256 校验失败"
else
log "未找到 $SUMS,跳过 sha256 校验"
fi
log "解压…"
unzip -qq -o "$TARGET" -d "$WORK_DIR" || { fail "解压失败"; exit 1; }
PACKAGE_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
else
PACKAGE_DIR="$TARGET"
fi
[ -n "$PACKAGE_DIR" ] && [ -d "$PACKAGE_DIR" ] || { fail "找不到解压后的包目录"; exit 1; }
log "包目录:$PACKAGE_DIR"
# ---- 2. 文件清单 ----
log "文件清单:"
for required in app-release.apk README.txt THIRD_PARTY.md contracts/VERSION; do
if [ -f "$PACKAGE_DIR/$required" ]; then
printf ' ✓ %s\n' "$required"
else
fail "缺少 $required"
fi
done
APK="$PACKAGE_DIR/app-release.apk"
if [ ! -f "$APK" ]; then
echo "(没有 APK,后续检查无法进行)" >&2
exit 1
fi
# ---- 3. APK 版本元数据 ----
log "APK 版本元数据(AndroidManifest):"
bash "$PROJECT_DIR/build/check-apk-version.sh" "$APK" "$EXPECTED" \
|| fail "APK 的 versionName/versionCode 不是 v$EXPECTED"
# ---- 4. ABI 与 MediaPipe native 库 ----
log "ABI 与 native 库:"
ABIS="$(unzip -l "$APK" | grep -oE 'lib/[^/]+/' | sed 's#lib/##;s#/##' | sort -u)"
ABIS_ONE_LINE="$(echo "$ABIS" | tr '\n' ' ')"
echo " 包含 ABI:$ABIS_ONE_LINE"
if [ "$ABIS" = "$(printf 'arm64-v8a\narmeabi-v7a')" ]; then
echo " ✓ ABI 恰为 arm64-v8a + armeabi-v7a(真机 ABI)"
else
fail "ABI 与预期不符(预期 arm64-v8a / armeabi-v7a)"
fi
if echo "$ABIS" | grep -qE "x86"; then
fail "release APK 含模拟器 ABI(包体翻倍且与 abiFilters 不符)"
fi
for abi in arm64-v8a armeabi-v7a; do
if unzip -l "$APK" | grep -q "lib/$abi/.*mediapipe"; then
echo " ✓ lib/$abi 下有 MediaPipe native 库"
else
fail "lib/$abi 下缺少 MediaPipe native 库(真机安装后必然初始化失败)"
fi
done
# ---- 5. 模型资源与 R8 保留 ----
log "MediaPipe 模型与 R8 保留:"
if unzip -l "$APK" | grep -q "assets/pose_landmarker_lite.task"; then
echo " ✓ MediaPipe 模型资源随包分发(assets/pose_landmarker_lite.task)"
else
fail "APK 缺少 MediaPipe 模型资源"
fi
DEX_HIT=0
for entry in $(unzip -Z1 "$APK" | grep -E '^classes[0-9]*\.dex$'); do
if unzip -p "$APK" "$entry" | grep -qa "poselandmarker/PoseLandmarker"; then
DEX_HIT=$((DEX_HIT + 1))
fi
done
if [ "$DEX_HIT" -gt 0 ]; then
echo " ✓ dex 中存在 poselandmarker/PoseLandmarker(R8 未整体裁掉 MediaPipe;命中 $DEX_HIT 个 dex)"
else
fail "dex 里找不到 PoseLandmarker(R8 把 MediaPipe 裁掉了,release 必崩)"
fi
# ---- 6. 验签(可选)----
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
APKSIGNER="$(ls -d "$SDK"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -1)"
if [ -n "$APKSIGNER" ]; then
log "签名校验($APKSIGNER):"
if "$APKSIGNER" verify "$APK" >"$WORK_DIR/apksigner.txt" 2>&1; then
echo " ✓ apksigner 验签通过(退出码 0)"
else
fail "apksigner 验签失败"
sed 's/^/ /' "$WORK_DIR/apksigner.txt" | tail -5
fi
else
log "未找到 apksigner,跳过验签(不影响结论)"
fi
# ---- 7. 契约快照与仓库一致 ----
log "包内契约快照:"
snapshot_ok=1
if [ -f "$PACKAGE_DIR/contracts/VERSION" ]; then
sed 's/^/ /' "$PACKAGE_DIR/contracts/VERSION"
# 与仓库当前的 contracts/VERSION 逐项比对:抓「APK 用旧契约构建、快照拷了新的」
while IFS= read -r line; do
case "$line" in ''|'#'*) continue ;; esac
key="${line%%:*}"; value="${line#*:}"
key="$(printf '%s' "$key" | tr -d '[:space:]')"
value="$(printf '%s' "$value" | tr -d '[:space:]')"
repo_value="$(sed -n "s/^${key}:[[:space:]]*//p" contracts/VERSION | head -1)"
if [ -z "$repo_value" ]; then
fail "仓库 contracts/VERSION 缺少 $key"
snapshot_ok=0
elif [ "$repo_value" != "$value" ]; then
fail "$key:包内 $value,仓库 $repo_value"
snapshot_ok=0
fi
done <"$PACKAGE_DIR/contracts/VERSION"
[ "$snapshot_ok" = 1 ] && echo " ✓ 包内契约快照与仓库 contracts/VERSION 一致"
else
fail "包内缺少 contracts/VERSION"
fi
echo ""
if [ "$FAILED" -ne 0 ]; then
echo "============================================================"
echo " 发布包级验收失败(工作目录:$WORK_DIR)"
echo "============================================================"
exit 1
fi
echo "============================================================"
echo " 发布包级验收全部通过(工作目录:$WORK_DIR)"
echo "============================================================"