文档 §11.2 要求「版本号单一来源 + 可核对」。本仓库原有校验只覆盖**构建配置**
(gradle verifyReleaseVersion 比 versionName ↔ project.yaml)。但版本号真正被
系统与用户看到的地方是 **APK 的 AndroidManifest**:安装界面、应用信息页、
dumpsys package 读的都是它。构建缓存、复用旧 APK、打包时拷错文件都会造成
「配置全对、产物是旧版」——配置层校验抓不到。
这不是假设:同批工作里 Windows 端 v0.2.1 的发布包就是配置文件全一致、
exe 属性页显示 0.1.0(opendance-windows/tasks/017-pe-version.md)。
新增:
* build/check_apk_version.py:直接解析 APK 内 AndroidManifest.xml(二进制 AXML)
的 versionName / versionCode。只依赖 Python 标准库,不需要 Android SDK/aapt2。
AXML 解析:顶层 ResChunk_header → 字符串池(UTF-8 与 UTF-16 都要处理)→
START_ELEMENT;属性名本身就在字符串池里,按文本匹配即可,无需资源映射表;
* build/check-apk-version.sh:薄包装,期望版本缺省取 project.yaml,
并校验 versionCode == 10000*主 + 100*次 + 修订(§11.1);
* build/verify.sh 步骤 3/3 新增「版本元数据」计分项(原先只 aapt2 打印一行);
* build/package-release.sh 打包前校验源 APK,不一致即 die。
交叉验证(与 aapt2 dump badging 逐项一致):
v0.1.0 APK -> versionCode 1 / versionName 0.1.0
v0.2.0 APK -> versionCode 200 / versionName 0.2.0
app/build/outputs/apk/release/app-release.apk -> 同 v0.2.0
反向验证(用真实旧产物当负例):
bash build/check-apk-version.sh dist-release/opendance-android-v0.1.0/app-release.apk
✗ versionName 是 '0.1.0',期望 '0.2.0'
✗ versionCode 是 1,期望 200
退出码 1
顺带修复:build/package-release.sh 第 31/33 行用 die 报版本不一致,但 die()
到第 40 行才定义。命中该分支时输出的是
build/package-release.sh: 行 31: die: 未找到命令 (退出码 127)
脚本确实中止了,但用户完全看不出真实原因。现把 log()/die() 移到首次使用之前:
[error] 版本号不一致:参数/versionName=9.9.9,project.yaml=0.2.0 (退出码 1)
未新发版本:APK 内容与 v0.2.0 完全一致(产物侧校验证明其 versionName/versionCode
本就正确),本次改动只涉及构建/校验脚本与文档,故不为它提版本号;
README.txt 的新增自检项会随下一次 Android 发布进入发布包。
101 lines
4.2 KiB
Bash
Executable File
101 lines
4.2 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+)"
|