更新godot ai到最新版本
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
@tool
|
||||
extends Node
|
||||
|
||||
## Verifies and stages the embedded canonical v4 release while the migration
|
||||
## capsule is still the enabled plugin. Every step runs in GDScript on the
|
||||
## editor's main thread through the same installer scripts the v4 tree ships
|
||||
## (`utils/release_verifier.gd`, `utils/update_installer.gd`); nothing here
|
||||
## spawns a process, and nothing here writes a live add-on file.
|
||||
|
||||
const PAYLOAD_ROOT := "res://addons/godot_ai/migration_payload"
|
||||
const ARCHIVE_NAME := "godot-ai-v4-plugin.zip"
|
||||
const MANIFEST_NAME := "godot-ai-v4-plugin.manifest.json"
|
||||
const SIGNATURE_NAME := "godot-ai-v4-plugin.manifest.sig"
|
||||
const VERIFIER_SCRIPT := "res://addons/godot_ai/utils/release_verifier.gd"
|
||||
const INSTALLER_SCRIPT := "res://addons/godot_ai/utils/update_installer.gd"
|
||||
const REPOSITORY := "hi-godot/godot-ai"
|
||||
const CHANNEL := "stable"
|
||||
const MAX_MANIFEST_BYTES := 1024 * 1024
|
||||
const SIGNATURE_BYTES := 512
|
||||
const PENDING_V3_UPDATE_SETTING := "godot_ai/pending_self_update_event"
|
||||
const INCOMPLETE_CAPSULE := (
|
||||
"The migration capsule is incomplete (%s). Download the release again, then click Retry migration."
|
||||
)
|
||||
|
||||
## Kept byte-identical to `utils/update_manager.gd::RELEASE_SIGNING_PUBLIC_KEY_PEM`
|
||||
## in the v4 tree; `tests/unit/test_release_support.py` pins the two copies
|
||||
## together. The capsule cannot preload the v4 updater to read it there.
|
||||
const RELEASE_SIGNING_PUBLIC_KEY_PEM := """-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr4OmbONFTONGFcXSUQ2p
|
||||
e54YaUhWDA75wxeDWhOc476vsdo53YnXEFT7EPr2hUKqeNxv++LqKOkFuAsxSNZy
|
||||
wBe6P1tmQA4Og6Ezv4CGnZdEj1uhlDJFK9ShQ29oWfC6bf/84625SvvBxZos2Br9
|
||||
yPKl7h5wzqDoeUSpv+f0ynTiC0i/HAUo/NQBlkgGwkomK2Fr3pP1VDxxq2xvgHSk
|
||||
lU6Qcomr9WjJxI+HkDN5tRPPn0pDrg6YFx2J18OfD8KIa/kMGxuXOcHlPyRYpjyu
|
||||
qTtg2oL0NyUIG+1TmJ3DcN4GlKC55eOrkfJ04vudS5pxdnUIFRmkGBXZLdaetoPc
|
||||
ixtlD4w6gi8KIH1CTG+/TtHP1KVdOogCWDcjRCAmMJPFZe6eEKXmGQUZDb9wfnbx
|
||||
h++XiVe5tq83BTLWmaFTy+fZbNo12uhNCNS1LJ42/yj+S1xvo0yMbkkNr1hIYk0P
|
||||
584XnBQeBSVJDf3667NZXaxnWv94K9zbb+1OvOvPwhbOdgi2Ymcw5QEOQIavtg86
|
||||
XLLcWzG+SJsycz1imikjv6sStWh8WHneKSTMq6A7V6PBj7oJyEJp10696BDw287k
|
||||
YlH+9VGqowPEMXpWX57wOBKiWb4K1kw1LfxjT8W1e/pcX9pJqiv0DkjTXUxo9CDG
|
||||
1X1+ZXBBR3MkGuFAOCjy0x8CAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
signal state_changed(message: String, failed: bool)
|
||||
signal prepared(package: Dictionary)
|
||||
|
||||
enum Step { IDLE, VERIFY, STAGE, DONE }
|
||||
|
||||
var _step := Step.IDLE
|
||||
var _manifest: Dictionary = {}
|
||||
var _manifest_sha256 := ""
|
||||
var _from_version := ""
|
||||
|
||||
|
||||
func start() -> void:
|
||||
if _step != Step.IDLE:
|
||||
return
|
||||
_from_version = _previous_version()
|
||||
_step = Step.VERIFY
|
||||
state_changed.emit("Verifying the signed Godot AI v4 release…", false)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
## One step per frame so the dock renders each status line before the
|
||||
## next synchronous hash/extract pass runs.
|
||||
match _step:
|
||||
Step.VERIFY:
|
||||
_verify()
|
||||
Step.STAGE:
|
||||
_stage()
|
||||
_:
|
||||
set_process(false)
|
||||
|
||||
|
||||
func _verify() -> void:
|
||||
var verifier := _load_script_with(VERIFIER_SCRIPT, "verify_manifest")
|
||||
if verifier == null or not _script_declares(verifier, "verify_archive"):
|
||||
_fail(INCOMPLETE_CAPSULE % "utils/release_verifier.gd is missing or invalid")
|
||||
return
|
||||
var manifest_bytes := _read_bounded(PAYLOAD_ROOT.path_join(MANIFEST_NAME), MAX_MANIFEST_BYTES)
|
||||
var signature := _read_bounded(PAYLOAD_ROOT.path_join(SIGNATURE_NAME), SIGNATURE_BYTES)
|
||||
if manifest_bytes.is_empty() or signature.size() != SIGNATURE_BYTES:
|
||||
_fail(INCOMPLETE_CAPSULE % "the embedded v4 manifest or signature is unreadable")
|
||||
return
|
||||
var expected := {"repository": REPOSITORY, "channel": CHANNEL}
|
||||
if not _from_version.is_empty():
|
||||
## Only the final v3 runners publish the pre-capsule plugin.cfg
|
||||
## version; older ones cannot, and the verifier must not be told a
|
||||
## guess.
|
||||
expected["current_version"] = _from_version
|
||||
var verified: Variant = verifier.call(
|
||||
"verify_manifest", manifest_bytes, signature, expected, RELEASE_SIGNING_PUBLIC_KEY_PEM
|
||||
)
|
||||
if not verified is Dictionary or not bool(verified.get("ok", false)):
|
||||
_fail(_error_of(verified, "The embedded v4 release manifest could not be verified."))
|
||||
return
|
||||
var manifest: Variant = (verified as Dictionary).get("manifest", {})
|
||||
if not manifest is Dictionary or str(manifest.get("version", "")).is_empty():
|
||||
_fail("The verified v4 manifest is missing its release version.")
|
||||
return
|
||||
_manifest = (manifest as Dictionary).duplicate(true)
|
||||
_manifest_sha256 = _sha256_hex(manifest_bytes)
|
||||
var archive_checked: Variant = verifier.call(
|
||||
"verify_archive", PAYLOAD_ROOT.path_join(ARCHIVE_NAME), _manifest
|
||||
)
|
||||
if not archive_checked is Dictionary or not bool(archive_checked.get("ok", false)):
|
||||
_fail(_error_of(archive_checked, "The embedded v4 archive does not match its signed manifest."))
|
||||
return
|
||||
_step = Step.STAGE
|
||||
state_changed.emit("Staging the verified Godot AI v4 tree…", false)
|
||||
|
||||
|
||||
func _stage() -> void:
|
||||
var installer := _load_script_with(INSTALLER_SCRIPT, "stage")
|
||||
if installer == null:
|
||||
_fail(INCOMPLETE_CAPSULE % "utils/update_installer.gd is missing or invalid")
|
||||
return
|
||||
var staged: Variant = installer.call("stage", PAYLOAD_ROOT.path_join(ARCHIVE_NAME), _manifest)
|
||||
if not staged is Dictionary or not bool(staged.get("ok", false)):
|
||||
_fail(_error_of(staged, "The verified v4 tree could not be staged."))
|
||||
return
|
||||
var stage_root := str((staged as Dictionary).get("stage_root", ""))
|
||||
var tree_sha256 := str((staged as Dictionary).get("tree_sha256", ""))
|
||||
if stage_root.is_empty() or tree_sha256.is_empty():
|
||||
_fail("The installer staged the v4 tree without reporting where or what it wrote.")
|
||||
return
|
||||
_step = Step.DONE
|
||||
set_process(false)
|
||||
state_changed.emit("Activating the verified Godot AI v4 tree…", false)
|
||||
prepared.emit({
|
||||
"stage_root": stage_root,
|
||||
"expected_tree_sha256": tree_sha256,
|
||||
"manifest_sha256": _manifest_sha256,
|
||||
"from_version": _from_version,
|
||||
"to_version": str(_manifest.get("version", "")),
|
||||
})
|
||||
|
||||
|
||||
func _fail(message: String) -> void:
|
||||
_step = Step.DONE
|
||||
set_process(false)
|
||||
state_changed.emit(message, true)
|
||||
|
||||
|
||||
## The pre-capsule plugin.cfg version, as recorded by the final v3 self-update
|
||||
## runner before it extracted the capsule over the tree. Empty when unknown.
|
||||
static func _previous_version() -> String:
|
||||
var settings := EditorInterface.get_editor_settings()
|
||||
if settings == null or not settings.has_setting(PENDING_V3_UPDATE_SETTING):
|
||||
return ""
|
||||
var parsed: Variant = JSON.parse_string(str(settings.get_setting(PENDING_V3_UPDATE_SETTING)))
|
||||
if not parsed is Dictionary:
|
||||
return ""
|
||||
var value := str(parsed.get("from_version", ""))
|
||||
return value if _is_pre_v4_version(value) else ""
|
||||
|
||||
|
||||
static func _is_pre_v4_version(value: String) -> bool:
|
||||
var expression := RegEx.new()
|
||||
return expression.compile("^[0-3]\\.\\d+\\.\\d+$") == OK and expression.search(value) != null
|
||||
|
||||
|
||||
## Load one of the v4 installer scripts by path. The capsule cannot name them
|
||||
## by `class_name`: those globals only exist once the v4 tree is live, and the
|
||||
## bridge must still parse when a truncated capsule lacks them entirely.
|
||||
static func _load_script_with(path: String, method: String) -> Script:
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
## Final v3 ships older files at some of these paths (utils/port_resolver.gd),
|
||||
## already compiled into the resource cache before the capsule was extracted
|
||||
## over the tree. Ignore the cache, deeply, so the capsule's copies and
|
||||
## their preloads are the ones that run.
|
||||
var loaded: Variant = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE_DEEP)
|
||||
if not loaded is Script or not _script_declares(loaded, method):
|
||||
return null
|
||||
return loaded
|
||||
|
||||
|
||||
static func _script_declares(script: Script, method: String) -> bool:
|
||||
for entry in script.get_script_method_list():
|
||||
if str(entry.get("name", "")) == method:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
static func _read_bounded(path: String, maximum: int) -> PackedByteArray:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
return PackedByteArray()
|
||||
var length := file.get_length()
|
||||
if length <= 0 or length > maximum:
|
||||
file.close()
|
||||
return PackedByteArray()
|
||||
var data := file.get_buffer(length)
|
||||
file.close()
|
||||
return data if data.size() == length else PackedByteArray()
|
||||
|
||||
|
||||
static func _sha256_hex(data: PackedByteArray) -> String:
|
||||
var context := HashingContext.new()
|
||||
context.start(HashingContext.HASH_SHA256)
|
||||
context.update(data)
|
||||
return context.finish().hex_encode()
|
||||
|
||||
|
||||
static func _error_of(result: Variant, fallback: String) -> String:
|
||||
if result is Dictionary:
|
||||
var error := str((result as Dictionary).get("error", "")).strip_edges()
|
||||
if not error.is_empty():
|
||||
return error
|
||||
return fallback
|
||||
Reference in New Issue
Block a user