Files
opendance-android/tasks/013-apk-version.md
T
zijunchiang 31b6ddd0fa test(packaging): APK 产物版本元数据校验(T-AND-013)+ 修 die 在定义前被调用
文档 §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 发布进入发布包。
2026-09-17 04:29:52 +08:00

4.2 KiB
Raw Blame History

T-AND-013:APK 产物版本元数据校验(+ 打包脚本 die 未定义修复)

  • 状态: done
  • 依赖: T-AND-009(构建)/ T-AND-010(版本号展示)
  • 文档: v1.6 §11.1 / §11.2
  • 契约: 不涉及(打包元数据)

一、为什么需要(同类失效的跨端复核)

文档 §11.2 要求「版本号单一来源 + 可核对」。本仓库原有两条校验:

  1. app/build.gradle.kts 的 verifyReleaseVersion 任务:比构建配置 (versionName ↔ project.yaml ↔ 契约快照);
  2. README.md / project.yaml 里的版本文本。

但版本号真正被系统与用户看到的地方是 APK 的 AndroidManifest: 安装界面、「应用信息」、dumpsys package、应用商店读的都是它。 构建缓存、复用旧 APK、打包时拷错文件,都会造成「配置全对、产物是旧版」—— 配置文件层面的校验抓不到。

这不是假设:同一批工作里,Windows 端 v0.2.1 的发布包就出现过配置文件全一致、 OpenDance.exe 属性页显示 0.1.0(export_presets.cfg 里的版本键没人管), 详见 opendance-windows/tasks/017-pe-version.md。因此本端也补一条读产物本身的校验。

二、实现

文件 作用
build/check_apk_version.py(新增) 直接解析 APK 内 AndroidManifest.xml(二进制 AXML)的 versionName / versionCode;只依赖 Python 标准库,不需要 Android SDK / aapt2
build/check-apk-version.sh(新增) 薄包装:找 python3 → 带期望版本时同时校验 versionCode == 10000*主 + 100*次 + 修订(§11.1);期望版本缺省取 project.yaml
build/package-release.sh 打包前校验源 APK 的版本元数据,不一致即 die
build/verify.sh 步骤 3/3 新增「版本元数据」校验项(原来只 aapt2 dump badging 打印一行,不判失败)

AXML 解析要点:顶层 ResChunk_header → 字符串池(UTF-8 / UTF-16 两种编码都要处理) → START_ELEMENT 节点;<manifest> 的属性名(versionName / versionCode) 本身就在字符串池里,按属性名文本匹配即可,无需解析资源映射表。

三、验证

与 aapt2 dump badging 交叉验证,三个真实 APK 完全一致:

APK aapt2 本脚本
dist-release/opendance-android-v0.1.0/app-release.apk versionCode='1' versionName='0.1.0' versionCode 1 / versionName 0.1.0 ✓
dist-release/opendance-android-v0.2.0/app-release.apk versionCode='200' versionName='0.2.0' versionCode 200 / versionName 0.2.0 ✓
app/build/outputs/apk/release/app-release.apk versionCode='200' versionName='0.2.0' 同上 ✓

反向验证(用真实旧产物当负例):

$ bash build/check-apk-version.sh dist-release/opendance-android-v0.1.0/app-release.apk
  ✗ APK 的 versionName 是 '0.1.0',期望 '0.2.0'(安装界面与应用信息页会显示错版本)
  ✗ APK 的 versionCode 是 1,期望 200(§11.1:10000*主 + 100*次 + 修订)
  退出码=1

四、顺带修复:package-release.sh 里 die 在定义之前被调用

build/package-release.sh 的第 31/33 行用 die 报版本不一致,但 die() 直到第 40 行 才定义。命中该分支时实际输出的是:

$ bash build/package-release.sh 9.9.9
build/package-release.sh: 行 31: die: 未找到命令
退出码=127

脚本确实中止了(set -e + 127),但用户看到的是 shell 报错,完全看不出真实原因。 现已把 log() / die() 的定义移到首次使用之前,修复后:

$ bash build/package-release.sh 9.9.9
[error] 版本号不一致:参数/versionName=9.9.9,project.yaml=0.2.0
退出码=1

五、验收

验收项 结果
能读出 APK 的 versionName / versionCode ✅ 与 aapt2 在 3 个真实 APK 上逐项一致
产物漂移会被拦住 ✅ 反向验证:v0.1.0 的 APK 对 0.2.0 期望 → 退出码 1,并指出具体不符项
打包与验证两条链路都接入 ✅ package-release.sh(打包前 die)/ verify.sh(步骤 3/3 计分)
不新增 SDK 依赖 ✅ 只用 Python 标准库;无 python3 时给出安装提示