#!/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}"