Files
opendance-windows/build/probe-release.gd
T
zijunchiang e4e534f2a2 test(accept): 发布包级端到端验收(读包内 pck 而非源码)+ T-WIN-018
`build/verify.sh` 跑的是仓库源码(`godot --path .`),交付给用户的是 zip 里的
OpenDance.pck + OpenDance.exe,差别全在「导出」这一步:导出预设漏资源、pck 没
重新生成、版本号没同步、契约快照与 pck 不一致 —— 都只在包里才暴露。
本仓 v0.2.1 的 PE 版本元数据就是这么翻的车。

新增:
  * build/probe-release.gd —— 包内运行时探针。用
    `godot --headless --main-pack <pck> --script <探针>` 只读包内资源,断言并
    quit(0/1):
      1) application/config/version == 期望版本(界面版本唯一来源,§11.1);
      2) AboutScreen.app_version() == 期望版本(「关于」清单真正显示的值);
      3) 主场景可加载、四个 autoload 都在场景树里;
      4) 15 个关键资源(UI/模型/场景/网络/评分)在包内都可加载;
      5) 包内 Credits.CONTRACT_VERSIONS 与包内 contracts/VERSION 快照逐项一致
         (抓「pck 用旧契约构建、快照却拷了新的」)。
  * build/accept-release.sh —— 一个 zip(或已解压目录)跑完:sha256 → 解压
    (deflate 用 unzip / LZMA 用 7z,并检查没有 .partN 残留)→ 文件清单 →
    check-pe-version.sh → 包内 pck 启动 180 帧无 SCRIPT ERROR → 探针。
  * package-release.sh 结尾打印下一步验收命令;README 补用法。

实测(对**已发布**的 v0.2.2 资产重新下载后跑):
  sha256 OK;PE 版本资源 0.2.2;包内 pck 启动 180 帧无错误;
  包内 version=0.2.2;AboutScreen.app_version()=0.2.2;主场景
  res://scenes/main.tscn;autoload 齐备;15 项关键资源可加载;
  pck 内契约表与包内 contracts/VERSION 一致 → 发布包级验收全部通过。

反向验证:
  * 篡改包内 contracts/VERSION 的 rest-api=0.4.0 →
    ✗ ["rest-api: pck=0.5.0 快照=0.4.0"],退出码 1;
  * 期望版本写 0.2.1 对 v0.2.2 的包 → 三处版本断言全失败,退出码 1。

顺带修掉脚本自身一个缺陷:SHA256SUMS 用相对路径 + cd 到包目录后被二次拼接成
dist-release/dist-release/SHA256SUMS,导致 sha256 校验假失败(其余检查仍全绿,
很容易被忽略)。首跑时正是它把「全部通过」变成了失败。

verify.sh 仍全绿(754 通过 / 0 失败 / 5 跳过)。
2026-09-17 04:50:40 +08:00

152 lines
5.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends SceneTree
## 发布产物验收探针(T-WIN-018,由 build/accept-release.sh 调用)
##
## 用法:
## godot --headless --main-pack <包内 OpenDance.pck> \
## --script /绝对路径/build/probe-release.gd \
## -- --expect-version 0.2.2 --snapshot /绝对路径/contracts/VERSION
##
## 为什么需要它:`build/verify.sh` 验证的是**仓库源码**(`godot --path .`),
## 而交付给用户的是 zip 里的 `OpenDance.pck`。差别全在「导出」这一步:
## 导出预设漏了资源、pck 没重新生成、版本号没同步、契约快照没更新……
## 都只在包里才暴露。本探针**只读包内资源**(`--main-pack` + `res://`),
## 所以它给出的版本号、契约表、autoload、主场景就是用户装到机器上的那一份。
##
## 断言(任一不满足即 quit(1)):
## 1. ProjectSettings 的 application/config/version == 期望版本
## (界面上「关于」清单与 .pck 都源自它 —— 文档 §11.1);
## 2. AboutScreen.app_version() == 期望版本(界面真正会显示的值);
## 3. 主场景非空且可加载,四个 autoload 都在场景树里;
## 4. 关键资源(UI / 模型 / 场景 / 网络 / 评分)在包内都可加载;
## 5. 包内 Credits.CONTRACT_VERSIONS 与**包内** contracts/VERSION 快照逐项一致
## —— 抓「pck 用旧契约构建、快照却拷了新的」这类漂移。
const KEY_RESOURCES := [
"res://src/ui/credits.gd",
"res://src/ui/about_screen.gd",
"res://src/ui/main.gd",
"res://src/ui/main_menu.gd",
"res://src/ui/scene_panel.gd",
"res://src/ui/source_panel.gd",
"res://src/model/humanoid_map.gd",
"res://src/model/vrm_loader.gd",
"res://src/scene/scene_runtime.gd",
"res://src/net/api_client.gd",
"res://src/net/catalog_types.gd",
"res://src/net/pose_server.gd",
"res://src/net/keypoint_frame.gd",
"res://src/score/dtw.gd",
"res://src/score/scoring.gd",
]
const EXPECTED_AUTOLOADS := ["AppConfig", "CatalogStore", "PoseReceiver", "ScoreService"]
var _failed := 0
func _check(ok: bool, text: String) -> void:
print(" %s %s" % ["✓" if ok else "✗", text])
if not ok:
_failed += 1
## 解析 contracts/VERSION(`key: value`,`#` 注释)
func _read_snapshot(path: String) -> Dictionary:
var out := {}
if path.is_empty() or not FileAccess.file_exists(path):
return out
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return out
while not file.eof_reached():
var line := file.get_line().strip_edges()
if line.is_empty() or line.begins_with("#"):
continue
var idx := line.find(":")
if idx > 0:
out[line.substr(0, idx).strip_edges()] = line.substr(idx + 1).strip_edges()
file.close()
return out
func _initialize() -> void:
var expect_version := ""
var snapshot_path := ""
var args := OS.get_cmdline_user_args()
var i := 0
while i < args.size():
if args[i] == "--expect-version" and i + 1 < args.size():
expect_version = args[i + 1]
i += 2
elif args[i] == "--snapshot" and i + 1 < args.size():
snapshot_path = args[i + 1]
i += 2
else:
i += 1
print("[accept] 包内运行时探针(--main-pack OpenDance.pck)")
var project_version := str(ProjectSettings.get_setting("application/config/version", ""))
print(" engine=%s project=%s main_scene=%s" % [
str(Engine.get_version_info().get("string", "?")),
project_version,
str(ProjectSettings.get_setting("application/run/main_scene", "")),
])
var about = load("res://src/ui/about_screen.gd")
var about_version := str(about.app_version()) if about != null else ""
var credits = load("res://src/ui/credits.gd")
var contract_versions: Dictionary = credits.CONTRACT_VERSIONS if credits != null else {}
if not expect_version.is_empty():
_check(project_version == expect_version,
"包内 application/config/version = %s(期望 %s)" % [project_version, expect_version])
_check(about_version == expect_version,
"AboutScreen.app_version() = %s(「关于」清单会显示这个值)" % about_version)
else:
print(" · 未指定 --expect-version,跳过版本比对(实际 %s)" % project_version)
var main_scene := str(ProjectSettings.get_setting("application/run/main_scene", ""))
_check(not main_scene.is_empty() and load(main_scene) != null, "主场景可加载:%s" % main_scene)
var autoloads := []
for child in root.get_children():
autoloads.append(str(child.name))
var missing := []
for name in EXPECTED_AUTOLOADS:
if not autoloads.has(name):
missing.append(name)
_check(missing.is_empty(), "autoload 齐备 %s(实际 %s)" % [str(EXPECTED_AUTOLOADS), str(autoloads)])
var unloadable := []
for path in KEY_RESOURCES:
if load(path) == null:
unloadable.append(path)
_check(unloadable.is_empty(), "关键资源全部可加载(%d 项)%s" % [
KEY_RESOURCES.size(), "" if unloadable.is_empty() else " 缺失:" + str(unloadable)])
var snapshot := _read_snapshot(snapshot_path)
if snapshot.is_empty():
print(" · 未提供 contracts/VERSION 快照(%s),跳过契约比对" % snapshot_path)
else:
print(" [contracts] 包内 Credits = ", JSON.stringify(contract_versions))
print(" [contracts] 包内快照文件 = ", JSON.stringify(snapshot))
var mismatch := []
for key in snapshot.keys():
if not contract_versions.has(key):
mismatch.append("%s 缺失" % key)
elif str(contract_versions[key]) != str(snapshot[key]):
mismatch.append("%s: pck=%s 快照=%s" % [key, contract_versions[key], snapshot[key]])
for key in contract_versions.keys():
if not snapshot.has(key):
mismatch.append("%s 只存在于 pck" % key)
_check(mismatch.is_empty(), "pck 内契约表与包内 contracts/VERSION 快照一致%s" % [
"" if mismatch.is_empty() else ":" + str(mismatch)])
if _failed > 0:
print("[accept] ✗ 探针发现 %d 项不符" % _failed)
quit(1)
return
print("[accept] ✓ 包内运行时探针全部通过")
quit(0)