fix(packaging): 发布包 exe 的 PE 版本资源必须等于发布版本(v0.2.2)

缺陷(v0.2.1 实测):把 opendance-windows-v0.2.1.zip 解开、在 Windows 上
「右键 → 属性 → 详细信息」,显示的是

    文件版本    0.1.0
    产品版本    0.1.0

而产物名、Git tag、README 全是 v0.2.1 —— 正是文档 §11.2 要禁止的
「不可核对的发布」。

根因:export_presets.cfg 的 application/file_version /
application/product_version 被硬编码在 0.1.0,而它们**不会**跟随
project.godot 的 application/config/version 自动更新;check-version.sh 当时
只比 project.godot ↔ project.yaml(两处一致),所以静态校验全绿、产物却是错的。
漂移发生在没人看的第三个位置 —— 校验位置少一个,等于没校验。

证据:
  strings -a -el OpenDance.exe  ->  FileVersion 0.1.0 / ProductVersion 0.1.0
  VS_FIXEDFILEINFO @0x67b976c   ->  FileVersion 0.1.0.0 / ProductVersion 0.0.0.0
对照组:官方模板 windows_release_x86_64.exe 的 FileVersion = 4.7.2,
说明本机 rcedit 改写是生效的,问题纯粹在预设的值本身。

修复:
  * build/sync-export-version.sh(新增):导出前把版本号写进导出预设,
    幂等、拒绝非 X.Y.Z;
  * build/check-pe-version.sh(新增):直接读 exe 的 PE 版本资源 ——
    VS_FIXEDFILEINFO(签名 0xFEEF04BD 后 16 字节,grep -abo 取偏移约 40 ms)
    与 StringFileInfo(strings -el,资源管理器属性页显示的就是它)。
    两处都查:实测固定信息里的 ProductVersion 恒为 0.0.0.0;
  * build/check-version.sh:新增导出预设两个键的校验;
  * build/export-windows.sh:导出前同步、导出后直接复核产物;
  * build/package-release.sh:打包前校验源 exe、解包后再校验包内 exe;
  * build/verify.sh:新增步骤 1/5「产物 PE 版本资源」(未导出则跳过);
  * tests/test_about.gd:新增 test_export_preset_version_matches_project_version。

反向验证:把预设 file_version 改回 0.1.0 →
  check-version.sh 退出码 1(报出两处不一致);
  无头测试 753 通过 / 1 失败,失败项正是
  「导出预设的 file_version 与版本号唯一来源一致」。

实测 v0.2.2 产物:PE FileVersion 0.2.2.0 / StringFileInfo 0.2.2,
版本号在 project.godot / project.yaml / export_presets.cfg / exe 元数据 /
产物名 / tag 六处一致。verify.sh 全绿(754 通过 / 0 失败 / 5 跳过)。

同时把 LZMA 压缩的取舍写清楚:deflate 包 38,248,818 B 仍超 IIS
30,000,000 B 上限,故 Release 资产仍是 LZMA 整包(26,628,529 B,不拆分卷);
若上限放开或改由 Web /static 分发,用 ZIP_METHOD=deflate 重打包即可得到
资源管理器可直接双击解压的包。
This commit is contained in:
2026-09-17 04:22:57 +08:00
parent 14ee5f7ec9
commit ba988f6ca6
13 changed files with 450 additions and 35 deletions
+116
View File
@@ -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 <exe> [期望版本 X.Y.Z]
# 不带期望版本时只打印实际值(排障用),退出码 0;
# 带期望版本时任一不一致即退出码 1。
# ============================================================
set -uo pipefail
EXE="${1:-}"
EXPECTED="${2:-}"
if [ -z "$EXE" ]; then
echo "用法:bash build/check-pe-version.sh <exe> [期望版本 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}"