diff --git a/README.md b/README.md index 2d46164..6747efa 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,36 @@ OpenDance Studio **阶段二**: Windows 客户端(Godot 4)。 | 跨实现交叉校验 | OK 关节角与 Web 服务端 TypeScript 实现**逐帧一致** | | 场景选择与物理落地 | OK `scenes[]` 可选、重力/风/摩擦/碰撞体落地并可日志核对 | | 连接保活与设备去重 | OK welcome/config 声明静默超时与心跳;应用层 ping/pong;按 deviceId 去重 | -| 发布整包 | OK 单个 `opendance-windows-v0.2.1.zip` + `SHA256SUMS`(不拆分卷) | -| 版本号展示 | OK 「关于」清单显示 `v0.2.0`,来源唯一(`project.godot`) | +| 发布整包 | OK 单个 `opendance-windows-v0.2.2.zip` + `SHA256SUMS`(不拆分卷) | +| 版本号展示 | OK 「关于」清单显示 `v0.2.2`;`project.godot` / `project.yaml` / `export_presets.cfg` / exe 的 PE 版本资源 / 产物名 / tag 全部一致 | | 场景物理与保活 | OK 新增真实 GLB 端到端用例 + 17 秒保活对照用例(751 项断言全过) | +## v0.2.2 修订(发布包 exe 的版本元数据) + +**缺陷**:v0.2.1 的包在 Windows 上「右键 → 属性 → 详细信息」显示 +**文件版本 0.1.0 / 产品版本 0.1.0**,而产物名与 Git tag 都是 v0.2.1。 + +**根因**:`export_presets.cfg` 的 `application/file_version` / +`application/product_version` 被硬编码在 `0.1.0`,而它们**不会**跟随 +`project.godot` 的 `application/config/version` 自动更新;`check-version.sh` +当时只比 `project.godot` ↔ `project.yaml`(两处一致),所以静态校验全绿、 +产物却是错的 —— 漂移发生在没人看的第三个位置。 + +**修复**: + +| 环节 | 做法 | +|--|--| +| 同步 | `build/sync-export-version.sh`(新增):导出前把版本号写进导出预设(幂等,拒绝非 `X.Y.Z`) | +| 静态防线 | `build/check-version.sh`:新增对导出预设两个键的校验 | +| 产物防线 | `build/check-pe-version.sh`(新增):直接读 exe 的 PE 版本资源 —— `VS_FIXEDFILEINFO`(签名 `0xFEEF04BD` 后 16 字节)+ `StringFileInfo`(`strings -el`,属性页显示的就是它)。两处都查:本机实测固定信息里的 `ProductVersion` 恒为 `0.0.0.0` | +| 打包 | `build/package-release.sh`:打包前校验源 exe、解包后再校验包内 exe | +| 一键验证 | `build/verify.sh`:新增步骤 1/5(未导出则跳过) | +| 单元测试 | `tests/test_about.gd`:预设两个键必须等于版本号唯一来源 | + +实测修复后产物:`VS_FIXEDFILEINFO FileVersion = 0.2.2.0`、 +`StringFileInfo FileVersion/ProductVersion = 0.2.2`。详见 +[`tasks/017-pe-version.md`](tasks/017-pe-version.md)。 + ## v0.2.1 修订(两处修复 + 一批"真机等价"测试) 1. **场景包围盒不再用 `global_transform`**:`SceneRuntime._compute_aabb()` 原本在 @@ -57,11 +83,12 @@ GODOT_BIN=/path/to/godot bash build/verify.sh GODOT_BIN=/path/to/godot bash build/export-windows.sh release # 打包发布产物(单个完整 zip + SHA256SUMS) -bash build/package-release.sh 0.2.1 +bash build/package-release.sh 0.2.2 ``` -当前版本:**v0.2.1**(`project.godot` 的 `application/config/version`, -与 `project.yaml`、导出产物名、Git tag、「关于」清单展示一致)。 +当前版本:**v0.2.2**(`project.godot` 的 `application/config/version`, +与 `project.yaml`、`export_presets.cfg`、导出产物名、Git tag、「关于」清单展示、 +以及 exe 的 PE 版本资源全部一致)。 详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md)。 @@ -142,7 +169,7 @@ GODOT_BIN=/path/to/godot bash build/export-windows.sh release powershell -ExecutionPolicy Bypass -File build\export-windows.ps1 # 打成可分发的 zip(单个完整包 + SHA256SUMS) -bash build/package-release.sh 0.2.1 +bash build/package-release.sh 0.2.2 ``` | 产物 | 位置 | 说明 | @@ -150,7 +177,7 @@ bash build/package-release.sh 0.2.1 | `OpenDance.exe` | `build/windows/` | 主程序,约 104 MB;脚本校验前 2 字节为 `MZ`(合法 PE) | | `OpenDance.pck` | `build/windows/` | 资源包,约 107 KB; **必须与 exe 同目录** | | `OpenDance.console.exe` | `build/windows/` | 控制台包装器,可能生成,非必需 | -| `opendance-windows-v0.2.1.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `README.md` + `contracts/` | +| `opendance-windows-v0.2.2.zip` | `dist-release/` | 发布包:exe + pck + `THIRD_PARTY.md` + `README.txt` + `README.md` + `contracts/` | | `SHA256SUMS` | `dist-release/` | 裸文件名,与 zip 同目录时可 `sha256sum -c SHA256SUMS` 直接校验 | 导出前必须安装与编辑器同版本的 export templates,否则脚本会报「未找到导出模板」。 @@ -247,12 +274,21 @@ src/ `zip.part0/1/2`(无法直接解压)。现改为**只上传单个完整包**,超限时打包脚本 直接告警并要求运维放宽上限,不再回退分卷;做法见 `opendance-web/deploy/proxy/README.md` 第九节。 -6. **v0.2.0 的 zip 用的是 LZMA 压缩**(`ZIP_METHOD=lzma` 打出来的),因为发布时 - IIS 上限尚未放开:整包从约 38 MB 降到约 26 MB 才能传上去。 - **代价:Windows 10 的资源管理器不支持 LZMA(method 14) 的 zip,必须用 - 7-Zip / WinRAR / PeaZip 解压**(Win11 24H2+ 起资源管理器原生支持 7z 等格式, - 但本项目未在 Windows 上实测)。若 IIS 上限已放开,请改用默认的 deflate 包 - (`bash build/package-release.sh 0.2.1`,产物约 38 MB,双击即可解压)。 +6. **v0.2.0 / v0.2.1 / v0.2.2 的 zip 用的是 LZMA 压缩**(`ZIP_METHOD=lzma` + 打出来的),因为这几次发布时 IIS 上限都尚未放开:整包从约 38 MB 降到约 26 MB + 才能传上去。**代价:Windows 10 的资源管理器不支持 LZMA(method 14) 的 zip, + 必须用 7-Zip / WinRAR / PeaZip 解压**(Win11 24H2+ 起资源管理器原生支持 7z + 等格式,但本项目未在 Windows 上实测)。 + 若 IIS 上限已放开、或你能把包放到别处分发(例如 Web 服务端的 `/static`, + 不受 IIS 那个限制约束),请改用资源管理器可直接双击解压的 deflate 包: + `ZIP_METHOD=deflate bash build/package-release.sh 0.2.2`(约 38 MB)。 +7. **发布包 exe 的版本元数据**(T-WIN-017):v0.2.1 之前在 Windows + 「属性 → 详细信息」里显示的是 `0.1.0`,因为 `export_presets.cfg` 的 + `application/file_version` / `application/product_version` 被硬编码且无人校验。 + v0.2.2 起由 `build/sync-export-version.sh` 在导出前同步、由 + `build/check-pe-version.sh` 直接读 exe 的 PE 版本资源复核 + (`VS_FIXEDFILEINFO` + `StringFileInfo` 两处都查), + `check-version.sh` / `package-release.sh` / `verify.sh` / `test_about.gd` 均已接入。 ## 第三方开源声明 diff --git a/README.txt b/README.txt index 9d8f227..79c0d42 100644 --- a/README.txt +++ b/README.txt @@ -1,5 +1,5 @@ ================================================================ -OpenDance Studio -- Windows 客户端 v0.2.1 使用说明(zip 为 LZMA 压缩,需用 7-Zip/WinRAR 解压) +OpenDance Studio -- Windows 客户端 v0.2.2 使用说明(zip 为 LZMA 压缩,需用 7-Zip/WinRAR 解压) ================================================================ 摄像头跳舞评分系统的核心体验端:接收 Android 辅助 App 通过 WebSocket @@ -8,6 +8,19 @@ OpenDance Studio -- Windows 客户端 v0.2.1 使用说明(zip 为 LZMA 压缩 本客户端只接收关键点坐标,不接收、不存储任何视频画面。 +v0.2.2 修订 + 1. 修复发布包 exe 的版本元数据: v0.2.1 的包在 Windows「右键 -> 属性 -> + 详细信息」里显示的是 文件版本 0.1.0 / 产品版本 0.1.0, 而产物名与 Git tag + 都是 v0.2.1。根因是 export_presets.cfg 的 application/file_version 与 + application/product_version 被硬编码成 0.1.0, 而它们不会跟随 project.godot + 的 application/config/version 自动更新。 + 2. 新增 build/sync-export-version.sh(导出前同步版本号到预设)与 + build/check-pe-version.sh(直接读 exe 的 PE 版本资源复核: VS_FIXEDFILEINFO + 的 FileVersion 与 StringFileInfo 的 FileVersion/ProductVersion 两处都查)。 + check-version.sh 增加预设校验, package-release.sh 在打包前与解包后各校验一次, + verify.sh 增加步骤 1/5, tests/test_about.gd 增加对应单测。 + 3. 现在本包显示: 文件版本 0.2.2 / 产品版本 0.2.2。 + v0.2.1 修订 1. 场景包围盒不再用 global_transform: SceneRuntime._compute_aabb() 原先在 _assemble() 里调用, 而那时节点还没入树 -> Godot 会打一行 ERROR @@ -116,24 +129,32 @@ OpenDance.exe 的用户看第一节即可。 打成发布包: - bash build/package-release.sh 0.2.1 + bash build/package-release.sh 0.2.2 得到 dist-release/ 下两个资产: - opendance-windows-v0.2.1.zip <- 单个完整包,禁止 .partN 分卷 + opendance-windows-v0.2.2.zip <- 单个完整包,禁止 .partN 分卷 SHA256SUMS <- 裸文件名,可直接 sha256sum -c 包里含 OpenDance.exe、OpenDance.pck、THIRD_PARTY.md、README.txt、 README.md、contracts/。打包脚本会解压自检(exe + pck 必须同目录、 - exe 的 PE 头为 MZ、pck 魔数为 GDPC)并打印包体与上传上限的对比。 + exe 的 PE 头为 MZ、pck 魔数为 GDPC、exe 的 PE 版本资源 = 发布版本) + 并打印包体与上传上限的对比。 注意:本项目 Gitea 前置 IIS 的默认上传上限是 30,000,000 字节 (约 28.6 MiB),超过会返回 HTTP 413。这时不要改成分卷, 请让运维放宽上限(做法见 opendance-web/deploy/proxy/README.md 第九节)。 - 发布 v0.2.0/v0.2.1 时该上限尚未放开,整包(约 38 MB)改用 LZMA 压缩到约 26 MB - 才传上去(打包时用 ZIP_METHOD=lzma)。**因此 v0.2.0/v0.2.1 的 zip 需要 - 7-Zip / WinRAR / PeaZip 解压 —— Windows 10 资源管理器不支持 LZMA 的 zip**; - 若上限已放开,请改用默认 deflate 包(双击即可解压)。 + v0.2.0/v0.2.1/v0.2.2 该上限都尚未放开,整包(deflate 约 38 MB)改用 + LZMA 压缩到约 26 MB 才传上去(打包时用 ZIP_METHOD=lzma)。 + **因此这几个版本的 zip 需要 7-Zip / WinRAR / PeaZip 解压 —— + Windows 10 资源管理器不支持 LZMA 的 zip**。 + + 如果你能把包放到别处分发(例如 Web 服务端的 /static,不受 IIS 那个 + 上限约束),建议改用资源管理器可直接双击解压的 deflate 包: + + ZIP_METHOD=deflate bash build/package-release.sh 0.2.2 # 约 38 MB + + 上限一旦放开,Release 资产也应换回 deflate 包。 7. 自检清单 diff --git a/build/check-pe-version.sh b/build/check-pe-version.sh new file mode 100755 index 0000000..b9815f1 --- /dev/null +++ b/build/check-pe-version.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# ============================================================ +# OpenDance Studio —— Windows 可执行文件的 PE 版本资源校验 +# +# 背景(真实缺陷):发布包里的 `OpenDance.exe` 是 Godot 用 +# `export_presets.cfg` 的 `application/file_version` / `application/product_version` +# 通过 rcedit 写进 PE `VERSIONINFO` 资源的。这两个键**当初被硬编码成 0.1.0 后再没人更新**, +# 于是 v0.2.1 的发布包在 Windows 上「右键 → 属性 → 详细信息」显示的是 +# 文件版本 0.1.0 / 产品版本 0.1.0 +# —— 产物名与 Gitea tag 是 v0.2.1,二进制元数据却是 0.1.0,正是文档 §11.2 +# 要禁止的那种「不可核对的发布」。光校验 project.godot / project.yaml 抓不到它: +# 那两处当时是一致的,漂移发生在**导出预设**里。 +# +# 因此本脚本直接读产物本身的两种版本元数据: +# 1. VS_FIXEDFILEINFO(二进制,权威、机器可读) +# FileVersionMS/LS、ProductVersionMS/LS —— 位于签名 0xFEEF04BD 之后 16 字节; +# 2. StringFileInfo(UTF-16LE 文本,资源管理器「详细信息」里显示的就是它) +# FileVersion / ProductVersion 键值对。 +# +# 用法: +# bash build/check-pe-version.sh [期望版本 X.Y.Z] +# 不带期望版本时只打印实际值(排障用),退出码 0; +# 带期望版本时任一不一致即退出码 1。 +# ============================================================ +set -uo pipefail + +EXE="${1:-}" +EXPECTED="${2:-}" + +if [ -z "$EXE" ]; then + echo "用法:bash build/check-pe-version.sh [期望版本 X.Y.Z]" >&2 + exit 2 +fi +if [ ! -f "$EXE" ]; then + echo "✗ 找不到文件:$EXE" >&2 + exit 2 +fi + +# ---- 1. VS_FIXEDFILEINFO ---- +# 签名 0xFEEF04BD(LE 字节序 bd 04 ef fe)在整个文件的 `VERSIONINFO` 资源里唯一。 +# 用 grep -abo 直接拿字节偏移(109 MB 文件约 40 ms),再 od 读 4 个 DWORD。 +SIG_OFFSETS=$(LC_ALL=C grep -abo "$(printf '\xbd\x04\xef\xfe')" "$EXE" 2>/dev/null | cut -d: -f1) +SIG_COUNT=$(printf '%s\n' "$SIG_OFFSETS" | grep -c . || true) + +if [ "$SIG_COUNT" -eq 0 ]; then + echo "✗ $EXE 里没有 VS_FIXEDFILEINFO 签名(0xFEEF04BD),PE 版本资源缺失" >&2 + exit 1 +fi +if [ "$SIG_COUNT" -gt 1 ]; then + echo "⚠ $EXE 里出现 $SIG_COUNT 处签名,取第一处($SIG_COUNT > 1 时结果可能不准)" >&2 +fi +SIG_OFFSET=$(printf '%s\n' "$SIG_OFFSETS" | head -1) + +read -r FV_MS FV_LS PV_MS PV_LS <<<"$(od -An -tu4 -j $((SIG_OFFSET + 16)) -N 16 "$EXE" | tr -s ' ')" +fmt_version() { printf '%s.%s.%s.%s' "$(( $1 >> 16 ))" "$(( $1 & 0xFFFF ))" "$(( $2 >> 16 ))" "$(( $2 & 0xFFFF ))"; } +FIXED_FILE_VERSION="$(fmt_version "$FV_MS" "$FV_LS")" +FIXED_PRODUCT_VERSION="$(fmt_version "$PV_MS" "$PV_LS")" + +# ---- 2. StringFileInfo(UTF-16LE 文本)---- +# `strings -el` 会把 UTF-16LE 字符串按出现顺序逐行列出,键与值相邻: +# FileVersion +# 0.1.0 +# 取键所在行的下一行即为值。缺 strings(binutils) 时降级为只查固定信息。 +string_value_of() { + local key="$1" text="$2" + printf '%s\n' "$text" | awk -v k="$key" '$0 == k { getline; print; exit }' +} +STRING_DUMP="" +if command -v strings >/dev/null 2>&1; then + STRING_DUMP="$(strings -a -el "$EXE" 2>/dev/null || true)" +fi +STR_FILE_VERSION="$(string_value_of FileVersion "$STRING_DUMP")" +STR_PRODUCT_VERSION="$(string_value_of ProductVersion "$STRING_DUMP")" +STR_PRODUCT_NAME="$(string_value_of ProductName "$STRING_DUMP")" +STR_COMPANY="$(string_value_of CompanyName "$STRING_DUMP")" + +echo " PE 版本资源($EXE):" +echo " VS_FIXEDFILEINFO FileVersion = ${FIXED_FILE_VERSION:-(未读到)}" +echo " VS_FIXEDFILEINFO ProductVersion = ${FIXED_PRODUCT_VERSION:-(未读到)}" +if [ -n "$STRING_DUMP" ]; then + echo " StringFileInfo FileVersion = ${STR_FILE_VERSION:-(未找到)}" + echo " StringFileInfo ProductVersion = ${STR_PRODUCT_VERSION:-(未找到)}" + echo " StringFileInfo ProductName = ${STR_PRODUCT_NAME:-(未找到)}" + echo " StringFileInfo CompanyName = ${STR_COMPANY:-(未找到)}" +else + echo " StringFileInfo (本机没有 strings/binutils,跳过文本项)" +fi + +if [ -z "$EXPECTED" ]; then + exit 0 +fi + +# ---- 3. 与期望版本比对 ---- +FAILED=0 +# 固定信息的第 4 段是 revision,rcedit 一律写 0,因此只比前三段 +fixed_prefix="${FIXED_FILE_VERSION%.*}" +if [ "$fixed_prefix" != "$EXPECTED" ]; then + echo " ✗ PE FileVersion 为 ${FIXED_FILE_VERSION:-(空)},期望 ${EXPECTED}.x(Windows 属性页会显示错版本)" >&2 + FAILED=1 +fi +if [ -n "$STRING_DUMP" ]; then + if [ "$STR_FILE_VERSION" != "$EXPECTED" ]; then + echo " ✗ 资源字符串 FileVersion 为 '${STR_FILE_VERSION:-(空)}',期望 '${EXPECTED}'" >&2 + FAILED=1 + fi + if [ "$STR_PRODUCT_VERSION" != "$EXPECTED" ]; then + echo " ✗ 资源字符串 ProductVersion 为 '${STR_PRODUCT_VERSION:-(空)}',期望 '${EXPECTED}'" >&2 + FAILED=1 + fi +fi +if [ "$FAILED" -ne 0 ]; then + echo " ✗ PE 版本资源与期望版本不一致 —— 请先跑 bash build/sync-export-version.sh 并重新导出" >&2 + exit 1 +fi + +echo " ✓ PE 版本资源一致:FileVersion ${FIXED_FILE_VERSION} / 资源字符串 ${EXPECTED}" diff --git a/build/check-version.sh b/build/check-version.sh index 9b370ac..0a155a1 100755 --- a/build/check-version.sh +++ b/build/check-version.sh @@ -20,15 +20,40 @@ cd "$PROJECT_DIR" GODOT_VERSION="$(sed -n 's/^config\/version="\(.*\)"$/\1/p' project.godot | head -1)" YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)" +PRESET="$PROJECT_DIR/export_presets.cfg" +PRESET_FILE_VERSION="$(sed -n 's/^application\/file_version="\(.*\)"$/\1/p' "$PRESET" 2>/dev/null | head -1)" +PRESET_PRODUCT_VERSION="$(sed -n 's/^application\/product_version="\(.*\)"$/\1/p' "$PRESET" 2>/dev/null | head -1)" EXPECTED="${1:-$YAML_VERSION}" -printf ' project.godot application/config/version = %s\n' "${GODOT_VERSION:-(未找到)}" -printf ' project.yaml version = %s\n' "${YAML_VERSION:-(未找到)}" -printf ' 期望版本 = %s\n' "${EXPECTED:-(未指定)}" +printf ' project.godot application/config/version = %s\n' "${GODOT_VERSION:-(未找到)}" +printf ' project.yaml version = %s\n' "${YAML_VERSION:-(未找到)}" +printf ' export_presets application/file_version = %s\n' "${PRESET_FILE_VERSION:-(未找到)}" +printf ' export_presets application/product_version= %s\n' "${PRESET_PRODUCT_VERSION:-(未找到)}" +printf ' 期望版本 = %s\n' "${EXPECTED:-(未指定)}" FAILED=0 [ -n "$GODOT_VERSION" ] || { echo " ✗ 无法从 project.godot 解析 config/version" >&2; FAILED=1; } [ -n "$YAML_VERSION" ] || { echo " ✗ 无法从 project.yaml 解析 version" >&2; FAILED=1; } +# 导出预设的 file_version / product_version 决定 OpenDance.exe 的 PE 版本资源, +# 即 Windows「右键 → 属性 → 详细信息」里显示的版本。它们**不会**随 project.godot +# 自动更新,曾经因此让 v0.2.1 的发布包显示 0.1.0。这里静态拦住; +# 产物那一侧再由 build/check-pe-version.sh 直接读 exe 复核。 +if [ ! -f "$PRESET" ]; then + echo " ✗ 找不到 export_presets.cfg(无法校验 PE 版本资源的来源)" >&2 + FAILED=1 +else + for PAIR in "file_version:$PRESET_FILE_VERSION" "product_version:$PRESET_PRODUCT_VERSION"; do + KEY="${PAIR%%:*}"; VALUE="${PAIR#*:}" + if [ -z "$VALUE" ]; then + echo " ✗ export_presets.cfg 缺少 application/${KEY}(导出后 PE 版本资源会沿用旧值)" >&2 + FAILED=1 + elif [ -n "$GODOT_VERSION" ] && [ "$VALUE" != "$GODOT_VERSION" ]; then + echo " ✗ 导出预设 application/${KEY}=${VALUE} 与 project.godot 的 ${GODOT_VERSION} 不一致" >&2 + echo " (Windows 属性页会显示 ${VALUE};跑 bash build/sync-export-version.sh 后重新导出)" >&2 + FAILED=1 + fi + done +fi if [ -n "$EXPECTED" ] && [ "$GODOT_VERSION" != "$EXPECTED" ]; then echo " ✗ project.godot 的版本与期望不一致(界面/About 会显示错版本)" >&2 FAILED=1 @@ -48,4 +73,4 @@ if [ "$FAILED" -ne 0 ]; then exit 1 fi -echo " ✓ 版本一致:v$GODOT_VERSION(project.godot = project.yaml${EXPECTED:+ = 期望值})" +echo " ✓ 版本一致:v$GODOT_VERSION(project.godot = project.yaml = export_presets.cfg${EXPECTED:+ = 期望值})" diff --git a/build/export-windows.sh b/build/export-windows.sh index cb30dbb..54dce05 100755 --- a/build/export-windows.sh +++ b/build/export-windows.sh @@ -67,6 +67,17 @@ if [ ! -d "$TEMPLATE_DIR" ] || [ -z "$(ls -A "$TEMPLATE_DIR" 2>/dev/null)" ]; th fi echo "[export] 导出模板:$(ls "$TEMPLATE_DIR" | tr '\n' ' ')" +# ---- 导出前:把版本号同步进导出预设 ---- +# export_presets.cfg 的 application/file_version / product_version 决定产物 exe 的 +# PE 版本资源(Windows「右键 → 属性 → 详细信息」里显示的版本),它们**不会** +# 跟随 project.godot 的 application/config/version 自动更新。不在这里同步, +# 导出的包就会带着旧版本号 —— v0.2.1 正是因此显示了 0.1.0。 +echo "[export] 同步版本号到导出预设…" +bash "$PROJECT_DIR/build/sync-export-version.sh" || { + echo "错误:导出预设版本同步失败。" >&2 + exit 1 +} + # ---- 确保资源已导入(缺失会让导出静默产出不完整的包)---- echo "[export] 导入资源…" "$GODOT_BIN" --headless --path "$PROJECT_DIR" --import >/dev/null 2>&1 || true @@ -105,11 +116,19 @@ if [ "$MAGIC" != "4d5a" ]; then exit 1 fi +# 校验 PE 版本资源 = 当前项目版本(文档 §11.2:产物元数据也必须可核对) +PROJECT_VERSION="$(sed -n 's/^config\/version="\(.*\)"$/\1/p' "$PROJECT_DIR/project.godot" | head -1)" +echo "[export] 校验产物 PE 版本资源(期望 $PROJECT_VERSION)…" +if ! bash "$PROJECT_DIR/build/check-pe-version.sh" "$EXE" "$PROJECT_VERSION"; then + echo "错误:$EXE 的 PE 版本资源与项目版本不一致。" >&2 + exit 1 +fi + SIZE_EXE=$(stat -c%s "$EXE") SIZE_PCK=$(stat -c%s "$PCK") echo "" echo "[export] 完成:" -echo " $EXE ($((SIZE_EXE / 1024)) KiB, PE 头校验通过)" +echo " $EXE ($((SIZE_EXE / 1024)) KiB, PE 头与版本资源校验通过)" echo " $PCK ($((SIZE_PCK / 1024)) KiB)" [ -f "$CONSOLE" ] && echo " $CONSOLE ($(( $(stat -c%s "$CONSOLE") / 1024 )) KiB)" echo "" diff --git a/build/package-release.sh b/build/package-release.sh index 71b3130..87b87de 100755 --- a/build/package-release.sh +++ b/build/package-release.sh @@ -59,6 +59,12 @@ PCK_MAGIC=$(head -c 4 "$PCK" | tr -d '\0') [ "$PCK_MAGIC" = "GDPC" ] || die "$PCK 不是合法的 Godot 资源包(头 '$PCK_MAGIC')" log "源产物校验通过:exe PE 头 MZ | pck 头 GDPC" +# 产物 exe 的 PE 版本资源必须是本次发布版本 —— Windows「属性 → 详细信息」显示的就是它。 +# v0.2.1 曾带着 0.1.0 发出去(导出预设里是硬编码的旧值,且没有任何环节复核产物)。 +bash "$PROJECT_DIR/build/check-pe-version.sh" "$EXE" "$VERSION" \ + || die "$EXE 的 PE 版本资源不是 v$VERSION;先跑 bash build/sync-export-version.sh $VERSION 再重新导出" +log "产物 PE 版本资源校验通过(=$VERSION)" + # ---- 2. 整理产物 ---- log "整理产物到 $STAGE …" rm -rf "$STAGE" @@ -123,6 +129,11 @@ CHECK_PCK=$(head -c 4 "$CHECK_DIR/$NAME/OpenDance.pck" | tr -d '\0') [ "$CHECK_PCK" = "GDPC" ] || die "解压后的 OpenDance.pck 不是合法 Godot 资源包" log "整包完整性自检通过(解压后 exe + pck 同目录,PE/GDPC 头均合法)" +# 再核一次**解压后**的 exe 版本资源:确认打包/解包环节没有换错文件 +bash "$PROJECT_DIR/build/check-pe-version.sh" "$CHECK_DIR/$NAME/OpenDance.exe" "$VERSION" \ + || die "包内 exe 的 PE 版本资源不是 v$VERSION" +log "包内 exe 的 PE 版本资源 = v$VERSION" + # pck 与 exe 必须同名同目录,否则 Godot 找不到资源 [ "$(basename "$CHECK_DIR/$NAME/OpenDance.exe" .exe)" = "$(basename "$CHECK_DIR/$NAME/OpenDance.pck" .pck)" ] \ || die "exe 与 pck 主文件名不一致,导出产物不可运行" diff --git a/build/sync-export-version.sh b/build/sync-export-version.sh new file mode 100755 index 0000000..99bf11a --- /dev/null +++ b/build/sync-export-version.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# ============================================================ +# OpenDance Studio —— 把当前版本号同步进 Godot 导出预设 +# +# 为什么需要它:`export_presets.cfg` 的 +# application/file_version / application/product_version +# 决定导出后 `OpenDance.exe` 的 PE `VERSIONINFO` 资源,也就是 Windows 上 +# 「右键 → 属性 → 详细信息」里显示的文件版本/产品版本。这两个键**不会** +# 跟着 `project.godot` 的 `application/config/version` 自动走: +# 它们当初被硬编码成 0.1.0 后一直没人改,导致 **v0.2.1 的包显示 0.1.0**。 +# +# 因此发布前必须显式同步,并由 `build/check-pe-version.sh` 在产物上复核。 +# +# 用法: +# bash build/sync-export-version.sh [版本号] # 缺省取 project.yaml 的 version +# ============================================================ +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_DIR" + +PRESET="$PROJECT_DIR/export_presets.cfg" +[ -f "$PRESET" ] || { echo "错误:缺少 $PRESET" >&2; exit 1; } + +VERSION="${1:-$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)}" +VERSION="${VERSION:-}" + +if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "错误:版本号必须形如 X.Y.Z(实际 '$VERSION')" >&2 + exit 1 +fi + +CHANGED=0 +for KEY in file_version product_version; do + LINE="application/${KEY}=\"${VERSION}\"" + if grep -qE "^application/${KEY}=" "$PRESET"; then + OLD=$(grep -E "^application/${KEY}=" "$PRESET" | head -1) + if [ "$OLD" != "$LINE" ]; then + # 用 sed 就地替换;值里只有数字和点,无需转义 + sed -i "s|^application/${KEY}=.*$|${LINE}|" "$PRESET" + echo " application/${KEY}: ${OLD#*=} → \"${VERSION}\"" + CHANGED=1 + else + echo " application/${KEY}: 已是 \"${VERSION}\"" + fi + else + # 预设里没有这一行就追加到 [preset.0.options] 段的末尾(application/ 键都在该段) + printf '%s\n' "$LINE" >>"$PRESET" + echo " application/${KEY}: 新增 \"${VERSION}\"" + CHANGED=1 + fi +done + +if [ "$CHANGED" -eq 1 ]; then + echo "✓ 导出预设已同步到 v${VERSION}(重新导出后 PE 资源才会更新)" +else + echo "✓ 导出预设已是 v${VERSION},无需改动" +fi diff --git a/build/verify.sh b/build/verify.sh index 8cad0b1..2d83ff0 100755 --- a/build/verify.sh +++ b/build/verify.sh @@ -26,7 +26,7 @@ fi FAILED=0 echo "============================================================" -echo " 0/4 版本号一致性(project.godot / project.yaml)" +echo " 0/5 版本号一致性(project.godot / project.yaml / export_presets.cfg)" echo "============================================================" if bash "$PROJECT_DIR/build/check-version.sh"; then : @@ -37,7 +37,24 @@ fi echo "" echo "============================================================" -echo " 1/4 脚本编译检查" +echo " 1/5 产物 PE 版本资源(Windows 属性页显示的版本)" +echo "============================================================" +EXPORTED_EXE="$PROJECT_DIR/build/windows/OpenDance.exe" +EXPECTED_VERSION="$(sed -n 's/^config\/version="\(.*\)"$/\1/p' "$PROJECT_DIR/project.godot" | head -1)" +if [ -f "$EXPORTED_EXE" ]; then + if bash "$PROJECT_DIR/build/check-pe-version.sh" "$EXPORTED_EXE" "$EXPECTED_VERSION"; then + : + else + echo "✗ 已导出 exe 的 PE 版本资源与 v$EXPECTED_VERSION 不一致(属性页会显示错版本)" + FAILED=1 + fi +else + echo "· 尚未导出 build/windows/OpenDance.exe,跳过(打包时 package-release.sh 会强制校验)" +fi + +echo "" +echo "============================================================" +echo " 2/5 脚本编译检查" echo "============================================================" IMPORT_LOG=$(mktemp) "$GODOT_BIN" --headless --path "$PROJECT_DIR" --import >"$IMPORT_LOG" 2>&1 @@ -52,7 +69,7 @@ rm -f "$IMPORT_LOG" echo "" echo "============================================================" -echo " 2/4 无头测试(单元 + 真实 API 集成)" +echo " 3/5 无头测试(单元 + 真实 API 集成)" echo "============================================================" TEST_LOG=$(mktemp) "$GODOT_BIN" --headless --path "$PROJECT_DIR" --script res://tests/run_tests.gd >"$TEST_LOG" 2>&1 @@ -69,7 +86,7 @@ rm -f "$TEST_LOG" echo "" echo "============================================================" -echo " 3/4 应用启动检查(180 帧)" +echo " 4/5 应用启动检查(180 帧)" echo "============================================================" RUN_LOG=$(mktemp) timeout 120 "$GODOT_BIN" --headless --path "$PROJECT_DIR" --quit-after 180 >"$RUN_LOG" 2>&1 diff --git a/export_presets.cfg b/export_presets.cfg index ed2294a..64d4edd 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -37,8 +37,8 @@ application/modify_resources=true application/icon="" application/console_wrapper_icon="" application/icon_interpolation=4 -application/file_version="0.1.0" -application/product_version="0.1.0" +application/file_version="0.2.2" +application/product_version="0.2.2" application/company_name="OpenDance Studio" application/product_name="OpenDance Studio" application/file_description="OpenDance Studio 摄像头跳舞评分客户端" diff --git a/project.godot b/project.godot index b6fd053..a87af9a 100644 --- a/project.godot +++ b/project.godot @@ -9,7 +9,7 @@ config_version=5 config/name="OpenDance Studio" config/description="摄像头跳舞评分系统 —— Windows 客户端。接收 Android 辅助 App 的 33 点骨骼关键点,驱动 VRM 角色并 DTW 实时评分。" -config/version="0.2.1" +config/version="0.2.2" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.7", "GL Compatibility") config/icon="res://icon.svg" diff --git a/project.yaml b/project.yaml index b42c562..5de8acc 100644 --- a/project.yaml +++ b/project.yaml @@ -1,5 +1,5 @@ project: opendance-windows-client -version: 0.2.1 +version: 0.2.2 repository: opendance-windows default_branch: main engine: Godot 4.7.2 @@ -90,9 +90,13 @@ tasks: file: tasks/015-keepalive-dedup.md scope: 连接保活 / 设备去重 / 状态机(契约 websocket-keypoints 0.2.0,与 T-AND-011 对齐) status: done + - id: T-WIN-017 + file: tasks/017-pe-version.md + scope: 发布包 exe 的 PE 版本资源(Windows 属性页显示的文件/产品版本)必须等于发布版本 + status: done deliverable: - artifact: opendance-windows-v0.2.1.zip + artifact: opendance-windows-v0.2.2.zip packaging: Godot 导出脚本(Linux 直接导出 / Windows 兜底) release_rules: # 文档 §5.7 硬性要求 - 单个完整包,禁止 .partN 分卷 diff --git a/tasks/017-pe-version.md b/tasks/017-pe-version.md new file mode 100644 index 0000000..0f4433d --- /dev/null +++ b/tasks/017-pe-version.md @@ -0,0 +1,92 @@ +# T-WIN-017:发布包 exe 的 PE 版本资源必须等于发布版本 + +- **状态**: done +- **依赖**: T-WIN-014(打包流程) +- **文档**: v1.6 §5.3 / §5.7 / §11.2 +- **契约**: 不涉及(纯打包元数据) + +## 缺陷(v0.2.1 实测) + +发布包 `opendance-windows-v0.2.1.zip` 里的 `OpenDance.exe`,在 Windows 上 +「右键 → 属性 → 详细信息」显示的是: + +``` +文件版本 0.1.0 +产品版本 0.1.0 +``` + +而产物名、Git tag、README 全都是 **v0.2.1** —— 正是文档 §11.2 要禁止的 +「不可核对的发布」。 + +### 根因 + +`export_presets.cfg` 里这两个键被**硬编码在 0.1.0**,且**没有任何环节读写或校验**: + +```ini +application/file_version="0.1.0" +application/product_version="0.1.0" +``` + +Godot 导出时用它们(经 rcedit)写进 exe 的 PE `VERSIONINFO` 资源。 +它们**不会**跟随 `project.godot` 的 `application/config/version` 自动更新 —— +于是每次只是改 `project.godot` + `project.yaml` 发版,二进制元数据一直停在 0.1.0。 + +`build/check-version.sh` 当时只比 `project.godot` ↔ `project.yaml`,两处一致, +所以**静态校验全绿、产物却是错的**:漂移发生在没人看的第三个位置。 + +### 证据 + +``` +$ strings -a -el opendance-windows-v0.2.1/OpenDance.exe | sed -n '3147,3151p' +FileVersion +0.1.0 +ProductVersion +0.1.0 + +$ bash build/check-pe-version.sh 0.2.1 + PE 版本资源(…/OpenDance.exe): + VS_FIXEDFILEINFO FileVersion = 0.1.0.0 + VS_FIXEDFILEINFO ProductVersion = 0.0.0.0 + StringFileInfo FileVersion = 0.1.0 + StringFileInfo ProductVersion = 0.1.0 + ✗ PE FileVersion 为 0.1.0.0,期望 0.2.1.x(Windows 属性页会显示错版本) + ✗ 资源字符串 FileVersion 为 '0.1.0',期望 '0.2.1' + ✗ 资源字符串 ProductVersion 为 '0.1.0',期望 '0.2.1' +``` + +(对照组:官方模板 `windows_release_x86_64.exe` 的 `FileVersion = 4.7.2`, +说明该机上的 rcedit 改写**是生效的**,问题纯粹出在预设的值本身。) + +## 修复 + +| 环节 | 文件 | 做法 | +|--|--|--| +| 同步 | `build/sync-export-version.sh`(新增) | 把版本号写进 `export_presets.cfg` 的 `application/file_version` / `application/product_version`;缺行则补行;幂等;拒绝非 `X.Y.Z` | +| 导出 | `build/export-windows.sh` | **导出前**先同步;**导出后**用 `check-pe-version.sh` 直接读 exe 复核 | +| 静态防线 | `build/check-version.sh` | 新增对 `export_presets.cfg` 两个键的校验(不等于项目版本即失败) | +| 产物防线 | `build/check-pe-version.sh`(新增) | 直接读 exe 的 PE 版本资源:`VS_FIXEDFILEINFO`(签名 `0xFEEF04BD` 之后 16 字节,`grep -abo` 取偏移约 40 ms)+ `StringFileInfo`(`strings -el`,资源管理器属性页显示的就是这两项) | +| 打包 | `build/package-release.sh` | 打包前校验源 exe、解包后再校验包内 exe(确认打包/解包没换错文件) | +| 一键验证 | `build/verify.sh` | 新增步骤 1/5:已导出 exe 的 PE 版本资源;未导出则跳过并提示(打包时会强制) | +| 单元测试 | `tests/test_about.gd` | 新增 `test_export_preset_version_matches_project_version`:预设两个键必须等于版本号唯一来源 | + +> 只校验 `VS_FIXEDFILEINFO` 是不够的:本机实测该结构里的 +> `ProductVersion` 恒为 `0.0.0.0`(rcedit 只填了字符串形式),而属性页显示的是 +> `StringFileInfo` 的文本值。因此两处都读、都对。 + +## 验收 + +| 验收项 | 结果 | +|--|--| +| 产物 exe 的 PE 版本资源 = 发布版本 | ✅ `check-pe-version.sh` 读 v0.2.2 产物通过(FileVersion 0.2.2.0 / 字符串 0.2.2) | +| 预设漂移能被拦住 | ✅ 反向验证:把预设改回 `0.1.0`,`check-version.sh` 与 `test_about.gd` 均失败 | +| 打包前/解包后都校验 | ✅ `package-release.sh` 两处 `check-pe-version.sh`,失败即 `die` | +| 不新增外部依赖 | ✅ 只用 `grep -abo` + `od` + `strings`(binutils),无 python/node 依赖 | + +## 复现命令 + +```bash +cd opendance-windows +bash build/check-pe-version.sh build/windows/OpenDance.exe 0.2.2 # 产物侧 +bash build/check-version.sh 0.2.2 # 配置侧 +bash build/sync-export-version.sh 0.2.2 # 需要修时 +``` diff --git a/tests/test_about.gd b/tests/test_about.gd index 8b31de8..92aeae4 100644 --- a/tests/test_about.gd +++ b/tests/test_about.gd @@ -62,3 +62,19 @@ func test_entry_scene_version_is_exported_into_build() -> void: var version := AboutScreen.app_version() contains("opendance-windows-v%s.zip" % version, version, "产物文件名包含版本号") ok(Credits.entry_count() >= 10, "开源声明条目仍齐备") + + +func test_export_preset_version_matches_project_version() -> void: + # 缺陷回归:export_presets.cfg 的 application/file_version 与 product_version + # 决定导出后 exe 的 PE VERSIONINFO 资源,也就是 Windows「右键 → 属性 → + # 详细信息」里显示的文件版本/产品版本。这两个键**不会**跟随 project.godot 的 + # application/config/version 自动更新,v0.2.1 的发布包因此显示的是 0.1.0 + # (产物名与 tag 都是 v0.2.1,二进制元数据却是 0.1.0)。 + # 产物侧由 build/check-pe-version.sh 直接读 exe 复核,这里做静态防线。 + var version := AboutScreen.app_version() + var preset := _read_text("res://export_presets.cfg") + ne(preset, "", "可读取 export_presets.cfg") + contains(preset, "application/file_version=\"%s\"" % version, + "导出预设的 file_version 与版本号唯一来源一致") + contains(preset, "application/product_version=\"%s\"" % version, + "导出预设的 product_version 与版本号唯一来源一致")