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