两个缺陷,都由上一轮的验收机制暴露:
1) 编码取整与契约参考实现不一致(T-WIN-022)
KeypointFrame.encode() 用 GDScript 的 roundi(半数远离零),而契约参考实现
(Web 的 keypoint-codec.ts)与**生产发送方 Android**(Kotlin Math.round)都是
「半数朝 +∞」:-2.5 分别是 -3 与 -2。实测在 12 个跨实现向量里 1 例 33 字节不同。
本端是接收方、encode() 只用于自测与协议回归,对产品无影响;但它让「跨实现一致性」
只能停留在宽松口径("差异仅限 .5 取整边界"),而宽松口径会退化成没人看的已知差异。
修复:新增 _round_like_js(v) = floor(v + 0.5),用于 4 处取整(坐标 ×3 + 置信度);
夹紧顺序对任意实数与参考实现等价。
收紧:accept-frame-vectors.sh 的编码口径改为「必须为 0 项差异」。
新增单测(+11 断言):_round_like_js 的 5 个取值、端到端 -0.5/32767 → 0(roundi
会给 -1,是有效判别用例),以及 test_reference_vectors_decode_and_encode_exactly
—— 直接读锁定的参考向量逐值解码 + 用 f32InputPoints 重编码与 f32Hex 逐字节比对,
让 verify.sh(只跑源码、无需 pck)也覆盖「Web ↔ Windows」这条边。
2) 产物里夹带开发/验收工具(T-WIN-023)
export_filter="all_resources" 且 exclude_filter 没有 build/*,于是每次新增
构建/验收脚本都被编译进 pck —— 包括**故意不合规**的坏服务端夹具 probe-ws-bad.gd
与 frame-vectors.json。实测 OpenDance.pck 315,464 B →(排除后)145,400 B,
即 54% 的 pck 是测试夹具。已发布的 v0.2.3 同样带着这些残留。
修复:exclude_filter 增加 build/*;
新增产物级防线 build/integration/list-pck.gd(遍历包内条目、标出
res://build/ tests/ _scratch/ dist-release/ 与 *.md 残留,有残留即退出码 1),
并接进 accept-release.sh(打印条目总数并断言「无开发残留」)——否则下次加个新
工具目录又会重演。顺带确认 pck 没有把上一次导出的 pck 自嵌套进去。
实测(v0.2.4 发布包,三条产物级验收全绿):
* accept-release.sh:pck 58 项无开发残留 / PE 版本资源 0.2.4 / pck 启动 180 帧无错 /
运行时探针(包内版本号 0.2.4、AboutScreen 显示值、15 项关键资源、契约表比对)
* accept-ws.sh:客户端 30 通过 / 0 失败 + 服务端侧 16 项事实
* accept-frame-vectors.sh:解码 12/12、396 点 0 差异,
**编码器字节与参考实现完全一致(0 项差异)**
* verify.sh:785 通过 / 0 失败(原 774)
踩坑:给 -0.5/32767 这类值写断言时,第一版按"理想有理数"期望(2.5/32767 → 3)而
失败 —— PackedVector3Array 存的是 float32,乘回 32767 后略小于半整数,与 JS 对同一
float32 输入的行为其实完全一致。断言必须基于实际存储精度,或改用有效判别用例。
200 lines
8.5 KiB
Bash
Executable File
200 lines
8.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# OpenDance Studio Windows 客户端 —— **发布包级端到端验收**(T-WIN-018)
|
||
#
|
||
# 为什么需要它:`build/verify.sh` 跑的是**仓库源码**(`godot --path .`),
|
||
# 而交付给用户的是 zip 里的 `OpenDance.pck` + `OpenDance.exe`。
|
||
# 差别全在「导出」这一步 —— 导出预设漏资源、pck 没重新生成、版本号没同步、
|
||
# 契约快照没更新,都只在包里才暴露。本项目已经因此翻过一次车:
|
||
# v0.2.1 的包解开后 exe 属性页显示 0.1.0(配置文件却全一致)。
|
||
#
|
||
# 本脚本对一个 zip(或已解压目录)跑完:
|
||
# 1. sha256 校验(有 SHA256SUMS 时);
|
||
# 2. 解压 + 文件清单完整性(exe / pck / README / THIRD_PARTY / contracts);
|
||
# 3. exe 的 **PE 版本资源** = 本次发布版本(build/check-pe-version.sh);
|
||
# 4. 用 Godot 直接加载**包内 pck** 跑 180 帧,断言无 SCRIPT ERROR;
|
||
# 5. 包内运行时探针(build/probe-release.gd):版本号、AboutScreen 显示值、
|
||
# 主场景、autoload、关键资源可加载、**pck 内契约表与包内 contracts/VERSION 一致**。
|
||
#
|
||
# 需要与本项目同版本的 Godot 4.7.x(导出模板要求的版本)。
|
||
#
|
||
# 用法:
|
||
# GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2.zip
|
||
# GODOT_BIN=/path/to/godot bash build/accept-release.sh dist-release/opendance-windows-v0.2.2
|
||
# GODOT_BIN=... bash build/accept-release.sh <zip> 0.2.2 # 显式指定期望版本
|
||
#
|
||
# 可选环境变量:
|
||
# WORK_DIR 解压目录(默认 mktemp -d)
|
||
# BOOT_FRAMES 启动检查帧数(默认 180)
|
||
# ============================================================
|
||
set -uo pipefail
|
||
|
||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||
cd "$PROJECT_DIR"
|
||
|
||
TARGET="${1:-}"
|
||
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
|
||
EXPECTED="${2:-$YAML_VERSION}"
|
||
WORK_DIR="${WORK_DIR:-$(mktemp -d)}"
|
||
BOOT_FRAMES="${BOOT_FRAMES:-180}"
|
||
|
||
FAILED=0
|
||
log() { printf '\033[1;34m[accept]\033[0m %s\n' "$*"; }
|
||
fail() { printf '\033[1;31m[accept] ✗ %s\033[0m\n' "$*" >&2; FAILED=1; }
|
||
|
||
if [ -z "$TARGET" ] || [ ! -e "$TARGET" ]; then
|
||
echo "用法:GODOT_BIN=/path/to/godot bash build/accept-release.sh <发布包 zip 或已解压目录> [期望版本]" >&2
|
||
exit 2
|
||
fi
|
||
if [ -z "$EXPECTED" ]; then
|
||
echo "✗ 无法确定期望版本(project.yaml 里没有 version,也没有显式传入)" >&2
|
||
exit 2
|
||
fi
|
||
|
||
log "目标:$TARGET"
|
||
log "期望版本:$EXPECTED"
|
||
log "工作目录:$WORK_DIR"
|
||
mkdir -p "$WORK_DIR"
|
||
|
||
# ---- 1. sha256 + 解压 ----
|
||
if [ -f "$TARGET" ]; then
|
||
# SHA256SUMS 里是**裸文件名**,所以校验时要把 cwd 切到包所在目录;
|
||
# 但 sums 文件本身必须用绝对路径 —— 否则 cd 之后相对路径会被二次拼接成
|
||
# dist-release/dist-release/SHA256SUMS(这个 bug 在本脚本首跑时就命中了)。
|
||
TARGET_DIR_ABS="$(cd "$(dirname "$TARGET")" && pwd)"
|
||
SUMS="$TARGET_DIR_ABS/SHA256SUMS"
|
||
if [ -f "$SUMS" ]; then
|
||
log "sha256 校验($SUMS):"
|
||
if ( cd "$TARGET_DIR_ABS" && sha256sum -c "$SUMS" --ignore-missing ); then
|
||
:
|
||
else
|
||
fail "sha256 校验失败"
|
||
fi
|
||
else
|
||
log "未找到 $SUMS,跳过 sha256 校验"
|
||
fi
|
||
|
||
log "解压…"
|
||
case "$TARGET" in
|
||
*.zip)
|
||
if unzip -qq -o "$TARGET" -d "$WORK_DIR" 2>/dev/null; then
|
||
log " 用 unzip 解压成功(deflate 包)"
|
||
elif command -v 7z >/dev/null 2>&1 && 7z x -y -o"$WORK_DIR" "$TARGET" >/dev/null; then
|
||
log " 用 7z 解压成功(LZMA 包 —— 注意 Windows 10 资源管理器不支持它)"
|
||
else
|
||
fail "解压失败(既不是可 unzip 的 deflate 包,也没有 7z 处理 LZMA)"
|
||
exit 1
|
||
fi
|
||
;;
|
||
*)
|
||
fail "只支持 .zip 或已解压目录"
|
||
exit 2
|
||
;;
|
||
esac
|
||
PACKAGE_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
||
else
|
||
PACKAGE_DIR="$TARGET"
|
||
fi
|
||
[ -n "$PACKAGE_DIR" ] && [ -d "$PACKAGE_DIR" ] || { fail "找不到解压后的包目录"; exit 1; }
|
||
log "包目录:$PACKAGE_DIR"
|
||
|
||
# ---- 2. 文件清单 ----
|
||
log "文件清单:"
|
||
for required in OpenDance.exe OpenDance.pck README.txt THIRD_PARTY.md contracts/VERSION; do
|
||
if [ -f "$PACKAGE_DIR/$required" ]; then
|
||
printf ' ✓ %s\n' "$required"
|
||
else
|
||
fail "缺少 $required"
|
||
fi
|
||
done
|
||
# exe 与 pck 必须同名同目录,否则 Godot 找不到资源
|
||
if [ "$(basename "$PACKAGE_DIR/OpenDance.exe" .exe)" != "$(basename "$PACKAGE_DIR/OpenDance.pck" .pck)" ]; then
|
||
fail "exe 与 pck 主文件名不一致"
|
||
fi
|
||
# 不允许出现历史分卷残留
|
||
if find "$PACKAGE_DIR" -maxdepth 1 -name "*.part[0-9]*" | grep -q .; then
|
||
fail "包内出现 .partN 分卷残留(发布规则要求单个完整包)"
|
||
fi
|
||
|
||
# ---- 3. exe 的 PE 版本资源 ----
|
||
log "产物 exe 的 PE 版本资源:"
|
||
if [ -f "$PACKAGE_DIR/OpenDance.exe" ]; then
|
||
bash "$PROJECT_DIR/build/check-pe-version.sh" "$PACKAGE_DIR/OpenDance.exe" "$EXPECTED" \
|
||
|| fail "exe 的 PE 版本资源不是 v$EXPECTED(Windows 属性页会显示错版本)"
|
||
fi
|
||
|
||
# ---- 4/5. 用 Godot 加载包内 pck ----
|
||
GODOT_BIN="${GODOT_BIN:-}"
|
||
if [ -z "$GODOT_BIN" ]; then
|
||
for candidate in godot godot4 Godot; do
|
||
if command -v "$candidate" >/dev/null 2>&1; then GODOT_BIN="$candidate"; break; fi
|
||
done
|
||
fi
|
||
if [ -z "$GODOT_BIN" ]; then
|
||
fail "未找到 Godot(本端验收需要与本项目同版本的 Godot 4.7.x;用 GODOT_BIN=<路径> 指定)"
|
||
echo "(其余检查已完成,见上)" >&2
|
||
exit 1
|
||
fi
|
||
log "Godot:$GODOT_BIN($("$GODOT_BIN" --version 2>/dev/null | head -1))"
|
||
|
||
PCK="$PACKAGE_DIR/OpenDance.pck"
|
||
if [ -f "$PCK" ]; then
|
||
log "包内 pck 启动检查($BOOT_FRAMES 帧,无头)…"
|
||
BOOT_LOG="$WORK_DIR/pck-boot.log"
|
||
timeout 300 "$GODOT_BIN" --headless --main-pack "$PCK" --quit-after "$BOOT_FRAMES" >"$BOOT_LOG" 2>&1
|
||
BOOT_STATUS=$?
|
||
if [ "$BOOT_STATUS" -ne 0 ]; then
|
||
fail "包内 pck 启动退出码 $BOOT_STATUS"
|
||
tail -20 "$BOOT_LOG" | sed 's/^/ /'
|
||
elif grep -qE "SCRIPT ERROR|Parse Error|Compile Error" "$BOOT_LOG"; then
|
||
fail "包内 pck 启动出现脚本错误"
|
||
grep -E "SCRIPT ERROR|Parse Error|Compile Error" "$BOOT_LOG" | head -10 | sed 's/^/ /'
|
||
else
|
||
echo " ✓ 包内 pck 启动 $BOOT_FRAMES 帧无错误(退出码 0)"
|
||
fi
|
||
|
||
# 产物内不得混入开发/验收工具(T-WIN-023):export_filter="all_resources" 只看
|
||
# exclude_filter,加了新的工具目录却忘了排除,就会把探针(乃至**故意不合规**的
|
||
# 坏服务端夹具)与向量文件打进 pck —— 实测曾占 pck 的 54%(315,464 B → 145,400 B)。
|
||
log "产物内开发残留检查(build/ tests/ _scratch/ *.md)…"
|
||
LIST_JSON="$WORK_DIR/pck-entries.json"
|
||
if timeout 120 "$GODOT_BIN" --headless --main-pack "$PCK" \
|
||
--script "$PROJECT_DIR/build/integration/list-pck.gd" \
|
||
-- --json --out "$LIST_JSON" >"$WORK_DIR/pck-list.log" 2>&1; then
|
||
node -e '
|
||
const fs = require("node:fs");
|
||
const r = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||
console.log(` ✓ pck 共 ${r.count} 项,无开发残留(build/ tests/ _scratch/ *.md)`);
|
||
' "$LIST_JSON" || fail "pck 残留清单解析失败"
|
||
else
|
||
fail "产物里混入了开发/验收残留(见 $WORK_DIR/pck-list.log)"
|
||
grep -E "✗|·" "$WORK_DIR/pck-list.log" | head -10 | sed 's/^/ /'
|
||
fi
|
||
|
||
log "包内运行时探针(版本号 / 界面显示值 / 契约表 / 资源可加载)…"
|
||
PROBE="$PROJECT_DIR/build/probe-release.gd"
|
||
PROBE_LOG="$WORK_DIR/probe.log"
|
||
# 探针自己断言并 quit(0/1),因此以**退出码**为准;日志单独落盘再打印,
|
||
# 避免把「grep 的退出码」误当成「探针的退出码」。
|
||
if timeout 300 "$GODOT_BIN" --headless --main-pack "$PCK" --script "$PROBE" \
|
||
-- --expect-version "$EXPECTED" --snapshot "$PACKAGE_DIR/contracts/VERSION" \
|
||
>"$PROBE_LOG" 2>&1; then
|
||
PROBE_OK=1
|
||
else
|
||
PROBE_OK=0
|
||
fi
|
||
grep -vE "^Godot Engine|^$" "$PROBE_LOG" | sed 's/^/ /'
|
||
[ "$PROBE_OK" = 1 ] || fail "包内运行时探针未通过"
|
||
fi
|
||
|
||
echo ""
|
||
if [ "$FAILED" -ne 0 ]; then
|
||
echo "============================================================"
|
||
echo " 发布包级验收失败(工作目录:$WORK_DIR)"
|
||
echo "============================================================"
|
||
exit 1
|
||
fi
|
||
echo "============================================================"
|
||
echo " 发布包级验收全部通过(工作目录:$WORK_DIR)"
|
||
echo "============================================================"
|