「发布包 == 打标签的树」此前只抽查几个文件,必然会漏。新增 build/check-tracked-release.py(纯标准库,直接读 LZMA zip):对包内**每一个**文件 按路径决定基准 —— OpenDance.exe / OpenDance.pck 比工作树 build/windows/ 的导出结果, 其余文件比 git show <tag>:<路径>;内容不一致 / 包内多余 / 构建产物缺失都会列出并退出 1。 已接入 build/accept-release.sh(第 0 步)与 build/package-release.sh 的提示。 实测(已发布的 v0.2.5 资产重新下载后): 包内 13 个文件:源码/文档 11(与 tag v0.2.5 比)+ 构建产物 2(与工作树导出比) ✓ 逐文件一致:0 处不一致 / 0 处多余 / 0 处缺失 反向验证:把包内 README.txt 追加一行后用 7z 重打成 LZMA zip → ✗ 内容不一致 1 处(README.txt),退出码 1。 踩坑:脚本第一版把 BUILD_OUTPUTS 的键一律当目录前缀(rel[len(prefix):]), 而本仓产物是单文件,拼出 build/windows/OpenDance.exe/ 导致误报「工作树缺失」; 现在区分「以 / 结尾的目录前缀」与「精确文件名」两种键。
216 lines
9.4 KiB
Bash
Executable File
216 lines
9.4 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"
|
||
|
||
# ---- 0. 逐文件核对:发布包 == 打标签的树(T-WIN-025)----
|
||
# 「发布包里的东西必须等于打标签的那棵树」此前只靠抽查几个文件,必然会漏。
|
||
# 本步骤对包内**每一个**文件核对:源码/文档比 tag,构建产物(exe/pck)比工作树导出结果。
|
||
CHECKER="$PROJECT_DIR/build/check-tracked-release.py"
|
||
if [ -f "$CHECKER" ] && command -v git >/dev/null 2>&1 && [ -f "$TARGET" ]; then
|
||
TAG="v$(basename "$TARGET" .zip | sed 's/^opendance-windows-v//')"
|
||
if git -C "$PROJECT_DIR" rev-parse --verify "${TAG}^{commit}" >/dev/null 2>&1; then
|
||
log "逐文件核对:发布包 == tag $TAG"
|
||
python3 "$CHECKER" "$TARGET" --tag "$TAG" || fail "发布包与 tag $TAG 不一致"
|
||
else
|
||
log "跳过逐文件核对(仓库里没有 tag $TAG;打标签后可手工跑 $CHECKER)"
|
||
fi
|
||
else
|
||
log "跳过逐文件核对(缺 checker / git / 目标是已解压目录)"
|
||
fi
|
||
|
||
# ---- 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 "============================================================"
|