`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 仍全绿。
104 lines
4.4 KiB
Bash
Executable File
104 lines
4.4 KiB
Bash
Executable File
#!/bin/bash
|
||
# ============================================================
|
||
# OpenDance Studio —— Android 辅助 App 发布打包
|
||
#
|
||
# 产物(文档 §6.3 / §11.2):
|
||
# opendance-android-vX.Y.Z.zip ← 单个完整包
|
||
# SHA256SUMS ← 裸文件名,下载目录内 sha256sum -c 直接可用
|
||
#
|
||
# 包内结构:
|
||
# opendance-android-vX.Y.Z/
|
||
# app-release.apk
|
||
# THIRD_PARTY.md
|
||
# README.txt README.md
|
||
# contracts/
|
||
#
|
||
# 版本号唯一来源是 app/build.gradle.kts 的 versionName;不传参数时自动读取,
|
||
# 并与 project.yaml 交叉校验(文档 §11.2:产物名 / tag / README / 界面必须一致)。
|
||
#
|
||
# 用法:bash build/package-release.sh [版本号]
|
||
# ============================================================
|
||
set -euo pipefail
|
||
|
||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||
cd "$PROJECT_DIR"
|
||
|
||
# 先定义 log/die:下面的版本号校验就会用到 die,函数必须先于调用定义,
|
||
# 否则命中的是 "die: 未找到命令"(退出码 127,看不出真实原因)。
|
||
log() { printf '\033[1;34m[package]\033[0m %s\n' "$*"; }
|
||
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
|
||
GRADLE_VERSION="$(sed -n 's/^[[:space:]]*versionName = "\(.*\)"/\1/p' app/build.gradle.kts | head -1)"
|
||
VERSION="${1:-$GRADLE_VERSION}"
|
||
VERSION="${VERSION:-0.2.0}"
|
||
[ -n "$YAML_VERSION" ] && [ "$VERSION" != "$YAML_VERSION" ] \
|
||
&& die "版本号不一致:参数/versionName=$VERSION,project.yaml=$YAML_VERSION"
|
||
[ -n "$GRADLE_VERSION" ] && [ "$VERSION" != "$GRADLE_VERSION" ] \
|
||
&& die "版本号不一致:参数=$VERSION,app/build.gradle.kts 的 versionName=$GRADLE_VERSION"
|
||
NAME="opendance-android-v${VERSION}"
|
||
OUT_DIR="$PROJECT_DIR/dist-release"
|
||
STAGE="$OUT_DIR/$NAME"
|
||
APK="app/build/outputs/apk/release/app-release.apk"
|
||
|
||
[ -f "$APK" ] || die "未找到 $APK,请先执行 bash build/build-apk.sh release"
|
||
|
||
# 源 APK 的 AndroidManifest 版本号必须是本次发布版本(文档 §11.2):
|
||
# 安装界面 / 应用信息页读的就是它,与 build.gradle.kts 的配置无关。
|
||
bash "$PROJECT_DIR/build/check-apk-version.sh" "$APK" "$VERSION" \
|
||
|| die "APK 的 versionName/versionCode 不是 v$VERSION;请用 bash build/build-apk.sh release 重新构建"
|
||
log "源 APK 版本元数据校验通过(=$VERSION)"
|
||
|
||
# APK 是 zip,校验魔数
|
||
MAGIC=$(head -c 2 "$APK" | od -An -tx1 | tr -d ' \n')
|
||
[ "$MAGIC" = "504b" ] || die "$APK 不是合法 APK(魔数 $MAGIC)"
|
||
|
||
log "整理产物到 $STAGE …"
|
||
rm -rf "$STAGE"
|
||
mkdir -p "$STAGE"
|
||
|
||
cp "$APK" "$STAGE/app-release.apk"
|
||
cp THIRD_PARTY.md README.txt "$STAGE/"
|
||
if [ -f README.md ]; then cp README.md "$STAGE/"; fi
|
||
cp -r contracts "$STAGE/contracts"
|
||
|
||
# ---- 完整性自检:APK 必须是合法 zip、含 MediaPipe 模型、ABI 只含真机 ABI ----
|
||
[ "$(head -c 2 "$APK" | od -An -tx1 | tr -d ' \n')" = "504b" ] || die "$APK 不是合法 APK"
|
||
unzip -l "$APK" | grep -q "assets/pose_landmarker_lite.task" || die "APK 缺少 MediaPipe 模型资源"
|
||
ABIS=$(unzip -l "$APK" | grep -oE "lib/[^/]+/" | sed 's#lib/##;s#/##' | sort -u | tr '\n' ' ')
|
||
log "APK 内 ABI:$ABIS"
|
||
for abi in arm64-v8a armeabi-v7a; do
|
||
echo "$ABIS" | grep -q "$abi" || die "APK 缺少 $abi 的 native 库"
|
||
done
|
||
if echo "$ABIS" | grep -qE "x86"; then
|
||
die "release APK 含模拟器 ABI($ABIS);请检查 abiFilters(T-AND-012)"
|
||
fi
|
||
unzip -l "$APK" | grep -q "lib/arm64-v8a/.*mediapipe" || die "arm64-v8a 下缺少 MediaPipe native 库"
|
||
|
||
ZIP="$OUT_DIR/$NAME.zip"
|
||
rm -f "$ZIP" "$OUT_DIR/SHA256SUMS"*
|
||
log "生成压缩包…"
|
||
( cd "$OUT_DIR" && zip -qr "$NAME.zip" "$NAME" )
|
||
|
||
# ---- SHA256SUMS(裸文件名,下载目录内可直接校验) ----
|
||
( cd "$OUT_DIR" && sha256sum "$NAME.zip" > SHA256SUMS )
|
||
log "完成:$ZIP($(du -h "$ZIP" | cut -f1))"
|
||
echo ""
|
||
echo "产物:"
|
||
echo " $ZIP"
|
||
echo " $OUT_DIR/SHA256SUMS"
|
||
if ( cd "$OUT_DIR" && sha256sum -c SHA256SUMS --status ); then
|
||
echo " sha256 校验:OK"
|
||
else
|
||
die "sha256 自校验失败"
|
||
fi
|
||
echo ""
|
||
unzip -l "$ZIP" | sed -n '4,20p'
|
||
echo ""
|
||
echo "分发:上传 $NAME.zip 与 SHA256SUMS 两个资产;用户下载后"
|
||
echo " sha256sum -c SHA256SUMS && unzip $NAME.zip"
|
||
echo " 再 adb install -r $NAME/app-release.apk(Android 10+)"
|
||
echo ""
|
||
echo "下一步(文档 §7.5:每次发布都要有一条读产物本身的验收):"
|
||
echo " bash build/accept-release.sh $ZIP"
|