Files
opendance-android/build/accept-release.sh
T
zijunchiang 0ee95c46e0 test(release): 逐文件核对「发布包 == 打标签的树」(T-AND-018)
新增 build/check_tracked_release.py(纯标准库):对包内每个文件按路径决定比较基准 ——
app-release.apk 比工作树构建结果,其余比 git show <tag>:<路径>;内容不一致 / 多余 /
构建产物缺失都会列出并退出 1。已接入 accept-release.sh(第 0 步)与 package-release.sh 提示。

实测(已发布的 v0.2.2 资产重新下载后):
  包内 12 个文件:源码/文档 11(与 tag v0.2.2 比)+ 构建产物 1(APK 比工作树构建)
  ✓ 逐文件一致:0 处不一致 / 0 处多余 / 0 处缺失
同行产品级验收(版本元数据 0.2.2/202、native 体检、契约快照、验签)同时全绿。

踩坑:第一版把 BUILD_OUTPUTS 的键一律当目录前缀,而 APK 是单文件,拼出
app-release.apk/ 导致「工作树缺失」误报;现区分目录前缀与精确文件名。
2026-09-18 03:10:35 +08:00

230 lines
9.3 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"
# ---- 0. 逐文件核对:发布包 == 打标签的树(T-AND-018)----
CHECKER="$PROJECT_DIR/build/check_tracked_release.py"
if [ -f "$CHECKER" ] && command -v git >/dev/null 2>&1 && [ -f "$TARGET" ]; then
TAG="v$(basename "$TARGET" .zip | sed 's/^opendance-android-v//')"
if git -C "$PROJECT_DIR" rev-parse --verify "${TAG}^{commit}" >/dev/null 2>&1; then
log "逐文件核对:发布包 == tag $TAG"
"${PYTHON_BIN:-python3}" "$CHECKER" "$TARGET" --tag "$TAG" || fail "发布包与 tag $TAG 不一致"
else
log "跳过逐文件核对(仓库里没有 tag $TAG)"
fi
else
log "跳过逐文件核对(缺 checker / git / 目标是已解压目录)"
fi
# ---- 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
# ---- 4b. native 库体检(T-AND-015:ABI / ELF 类型 / 依赖闭包 / 16 KB 页对齐)----
log "native 库体检(ELF 类型 / DT_NEEDED 闭包 / 16 KB 页对齐):"
NPY="${PYTHON_BIN:-}"
if [ -z "$NPY" ]; then
for candidate in python3 python; do command -v "$candidate" >/dev/null 2>&1 && { NPY="$candidate"; break; }; done
fi
if [ -n "$NPY" ]; then
"$NPY" "$PROJECT_DIR/build/check_native_libs.py" "$APK" \
--expected-abis "$(echo "$ABIS" | tr '\n' ',')" || fail "native 库体检未通过"
else
fail "未找到 python3,无法做 native 库体检(apt-get install -y python3)"
fi
# 16 KB 页对齐的 zip 层检查(Android 15 起部分设备页 16 KB)
SDK_FOR_ZIPALIGN="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}"
ZIPALIGN="$(ls -d "$SDK_FOR_ZIPALIGN"/build-tools/*/zipalign 2>/dev/null | sort -V | tail -1)"
if [ -n "$ZIPALIGN" ]; then
if "$ZIPALIGN" -c -P 16 -v 4 "$APK" >"$WORK_DIR/zipalign.txt" 2>&1; then
echo " ✓ zipalign -c -P 16 -v 4 通过(4 字节对齐 + 16 KB 页对齐)"
else
fail "zipalign -P 16 校验失败(见 $WORK_DIR/zipalign.txt)"
fi
else
log "未找到 zipalign,跳过 zip 对齐检查(不影响结论)"
fi
# ---- 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 "============================================================"