`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 跳过)。
152 lines
5.8 KiB
GDScript
152 lines
5.8 KiB
GDScript
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)
|