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)