From 06ffa7b6ac26bba1116caf1718f10c174c03f1c1 Mon Sep 17 00:00:00 2001 From: suzmii Date: Mon, 28 Sep 2026 00:50:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E5=A4=8D=E6=B5=8B=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20M3=EF=BC=9Apayload=20=E9=97=A8=E6=A7=9B=E4=B8=8E=E4=BA=A4?= =?UTF-8?q?=E4=BB=98=E3=80=81=E7=BC=93=E5=AD=98=E9=94=AE=E3=80=81=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6=E5=86=99=E5=8F=8D=E4=B8=8E=E9=97=A8=E7=A6=81=E8=A6=86?= =?UTF-8?q?=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P0:native/payload 补 features=["cocos-editor-injection"](与 nativePayloads 同门槛,保持迁移前语义),Windows 默认 feature 下不再无条件要求该目录 - P0:copyNativePayloads 真正改用 destinationFileName(此前声明了却仍写 sourceFileName,落盘成 cocos_editor_bridge.dll 而运行时找 cocos-editor-bridge.dll) - P1:payload 交付移到插件缓存短路之前(先默认构建、之后开 injection 不再整条跳过),并把交付物(含缺失态)纳入 pluginSourceFingerprint,删掉它必须导致重建 - P2:修正 pluginTreeMatches 里被写反的条件(原为 || 短路,导致内容比对整体失效) - P2:门禁新增 validateExtendedDeclarations,覆盖 prepareSteps/nativePayloads 的字段与引用完整性(含 destinationFileName) - P2:删除 godot_bundle 的 stage/source_files 及其单测(构建脚本不再有调用方),清掉遗留的 PathBuf 导入 - P3:CLI 支持 --features=a,b 形式 - 验证:工具用例 13/13;声明门禁通过;cargo fmt/cargo check 通过 --- .../scripts/check-package-layout.mjs | 74 +++++++++++ .../scripts/prepare-bundled-resources.mjs | 26 +++- .../src-tauri/build_support/godot_bundle.rs | 115 +----------------- .../build_support/package-layout.generated.rs | 2 +- .../build_support/package-layout.json | 5 +- .../src-tauri/build_support/package_layout.rs | 4 +- 6 files changed, 104 insertions(+), 122 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/check-package-layout.mjs b/apps/ai-game-creator-shell/scripts/check-package-layout.mjs index a8eda97e1..f7d10f97d 100755 --- a/apps/ai-game-creator-shell/scripts/check-package-layout.mjs +++ b/apps/ai-game-creator-shell/scripts/check-package-layout.mjs @@ -487,8 +487,82 @@ function appLockedCodexVersion() { return declared.replace(/^[\^~]/, ''); } +/// 新 section(prepareSteps / nativePayloads / prepared 来源)不参与 Rust 生成物, +/// 必须在门禁里单独把关,避免「声明了却没人用」或引用到不存在的准备步骤。 +function validateExtendedDeclarations() { + const root = expectObject( + JSON.parse(readFileSync(DECLARATION_PATH, 'utf8')), + 'root', + ); + const plugins = expectObject(root.plugins, 'plugins'); + const steps = expectArray(plugins.prepareSteps ?? [], 'plugins.prepareSteps'); + const names = new Set(); + for (const [index, raw] of steps.entries()) { + const at = `plugins.prepareSteps[${index}]`; + const step = expectObject(raw, at); + const name = expectString(step.name, `${at}.name`); + if (names.has(name)) { + fail(`${at}.name 重复:${name}`); + } + names.add(name); + const kind = expectString(step.kind, `${at}.kind`); + if (kind !== 'powershell' && kind !== 'cargo') { + fail(`${at}.kind 只能是 powershell 或 cargo`); + } + if (kind === 'powershell') { + expectString(step.workingDirectory, `${at}.workingDirectory`); + expectString(step.scriptFileName, `${at}.scriptFileName`); + } else { + expectString(step.packageDirectory, `${at}.packageDirectory`); + } + expectStringArray(step.requiredOutputs ?? [], `${at}.requiredOutputs`); + } + const payloads = expectArray( + plugins.nativePayloads ?? [], + 'plugins.nativePayloads', + ); + for (const [index, raw] of payloads.entries()) { + const at = `plugins.nativePayloads[${index}]`; + const payload = expectObject(raw, at); + expectString(payload.plugin, `${at}.plugin`); + expectString(payload.prepare, `${at}.prepare`); + expectString(payload.sourceFileName, `${at}.sourceFileName`); + expectString(payload.destinationFileName, `${at}.destinationFileName`); + expectString( + payload.destinationSubdirectory, + `${at}.destinationSubdirectory`, + ); + if (!names.has(payload.prepare)) { + fail(`${at}.prepare 引用了未声明的准备步骤:${payload.prepare}`); + } + } + const referenced = [ + ...expectArray(plugins.subdirectories ?? [], 'plugins.subdirectories') + .filter( + (entry) => expectObject(entry, 'subdirectory').origin === 'prepared', + ) + .map((entry) => [entry.path, entry.prepare]), + ...expectArray(plugins.libraryStaging ?? [], 'plugins.libraryStaging').map( + (entry) => [entry.sourceSubdirectory, entry.prepare], + ), + ...payloads.map((entry) => [ + `${entry.plugin}/${entry.destinationSubdirectory}`, + entry.prepare, + ]), + ]; + for (const [label, prepare] of referenced) { + if (!prepare) { + fail(`prepared 来源的条目缺少 prepare 声明:${label}`); + } + if (!names.has(prepare)) { + fail(`${label} 引用了未声明的准备步骤:${prepare}`); + } + } +} + function main() { const declaration = parseDeclaration(readFileSync(DECLARATION_PATH, 'utf8')); + validateExtendedDeclarations(); const lockedVersion = appLockedCodexVersion(); if (lockedVersion !== declaration.codex.version) { fail( diff --git a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs index 9b6a9508b..2b5e5dae8 100755 --- a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs @@ -710,8 +710,8 @@ function pluginTreeMatches( } for (const subdirectory of declaration.plugins.subdirectories) { if ( - subdirectoryAppliesToPlugin(subdirectory, plugin.name) || - subdirectoryEnabled(subdirectory, target, features) + !subdirectoryAppliesToPlugin(subdirectory, plugin.name) || + !subdirectoryEnabled(subdirectory, target, features) ) { continue; } @@ -1052,6 +1052,8 @@ function copyNativePayloads({ `Cocos bridge native payload 未构建:${candidates.map((candidate) => path.relative(repoRoot, candidate)).join(';')}`, ); } + const destinationName = + payload.destinationFileName ?? payload.sourceFileName; copyFilePreservingMode( source, path.join( @@ -1059,7 +1061,7 @@ function copyNativePayloads({ 'plugins', payload.plugin, payload.destinationSubdirectory, - payload.sourceFileName, + destinationName, ), ); copyFilePreservingMode( @@ -1068,7 +1070,7 @@ function copyNativePayloads({ staging, payload.plugin, payload.destinationSubdirectory, - payload.sourceFileName, + destinationName, ), ); } @@ -1089,6 +1091,18 @@ function preparePlugins({ destinationRoot, declaration.plugins.destinationDirectory, ); + // payload 交付不能排在缓存短路之后:否则「先默认构建、之后开 injection」会把交付整条跳过。 + if (existsSync(destination)) { + copyNativePayloads({ + declaration, + repoRoot, + srcTauriRoot: destinationRoot, + staging: destination, + target, + features, + profile, + }); + } const fingerprint = pluginSourceFingerprint( declaration, plugins, @@ -1244,6 +1258,10 @@ function parseArguments(argv) { index += 1; } else if (value === '--dry-run') { args.dryRun = true; + } else if (value.startsWith('--features=')) { + args.features = new Set( + value.slice('--features='.length).split(',').filter(Boolean), + ); } else if (value === '--features') { args.features = new Set( String(argv[index + 1] ?? '') diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/godot_bundle.rs b/apps/ai-game-creator-shell/src-tauri/build_support/godot_bundle.rs index 616ad5b30..02af4c3a5 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/godot_bundle.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/godot_bundle.rs @@ -1,6 +1,6 @@ use sha2::{Digest, Sha256}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; // 共享随包资源声明:Godot 随包文件清单与构建期校验共用同一份来源。 #[allow(dead_code)] @@ -74,39 +74,6 @@ pub fn validate(root: &Path) -> Result)>, String> { Ok(files) } -pub fn stage(root: &Path, destination: &Path, target: &str, enabled: bool) -> Result<(), String> { - if target != "x86_64-pc-windows-msvc" || !enabled { - return Ok(()); - } - for (relative, bytes) in validate(root)? { - let path = destination.join(relative); - fs::create_dir_all(path.parent().expect("Godot resource parent")) - .map_err(|error| format!("创建 Godot 资源目录失败:{error}"))?; - fs::write(&path, bytes).map_err(|error| format!("写入 Godot 资源失败:{error}"))?; - } - Ok(()) -} - -pub fn source_files(root: &Path) -> Result, String> { - plain_metadata(root)?; - let mut sources = Vec::new(); - for entry in fs::read_dir(root).map_err(|error| format!("读取 Godot 源码失败:{error}"))? - { - let entry = entry.map_err(|error| format!("读取 Godot 源码目录项失败:{error}"))?; - if matches!(entry.file_name().to_str(), Some("bin" | ".build")) { - continue; - } - let metadata = plain_metadata(&entry.path())?; - if metadata.is_dir() { - sources.extend(source_files(&entry.path())?); - } else if metadata.is_file() { - sources.push(entry.path()); - } - } - sources.sort(); - Ok(sources) -} - #[cfg(test)] mod tests { use super::*; @@ -132,84 +99,4 @@ mod tests { ) .unwrap(); } - - #[test] - fn stage_only_verified_windows_runtime_and_not_build_inputs() { - let source = tempfile::tempdir().unwrap(); - let destination = tempfile::tempdir().unwrap(); - fixture(source.path()); - fs::write(source.path().join("bridge.gd"), "source").unwrap(); - fs::write(source.path().join("bin/win-x64/extra.dll"), "excluded").unwrap(); - stage( - source.path(), - destination.path(), - "x86_64-pc-windows-msvc", - true, - ) - .unwrap(); - for relative in BUNDLE_FILES { - assert_eq!( - fs::read(source.path().join(relative)).unwrap(), - fs::read(destination.path().join(relative)).unwrap() - ); - } - assert!(!destination.path().join("bridge.gd").exists()); - assert!(!destination.path().join("bin/win-x64/extra.dll").exists()); - } - - #[test] - fn unsupported_or_disabled_targets_need_no_native_artifacts() { - let destination = tempfile::tempdir().unwrap(); - for (target, enabled) in [ - ("aarch64-apple-darwin", true), - ("x86_64-apple-darwin", true), - ("x86_64-unknown-linux-gnu", true), - ("aarch64-pc-windows-msvc", true), - ("x86_64-pc-windows-msvc", false), - ] { - stage( - Path::new("missing-godot-native"), - destination.path(), - target, - enabled, - ) - .unwrap(); - assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0); - } - } - - #[test] - fn incomplete_or_tampered_bundle_fails_before_copying() { - let source = tempfile::tempdir().unwrap(); - let destination = tempfile::tempdir().unwrap(); - fixture(source.path()); - fs::write(source.path().join(BUNDLE_FILES[0]), b"tampered").unwrap(); - assert!(stage( - source.path(), - destination.path(), - "x86_64-pc-windows-msvc", - true - ) - .unwrap_err() - .contains("SHA256")); - assert_eq!(fs::read_dir(destination.path()).unwrap().count(), 0); - fixture(source.path()); - fs::remove_file(source.path().join("vendor/LICENSE.txt")).unwrap(); - assert!(validate(source.path()).is_err()); - } - - #[test] - fn source_watch_list_excludes_build_outputs() { - let source = tempfile::tempdir().unwrap(); - fixture(source.path()); - fs::create_dir(source.path().join(".build")).unwrap(); - fs::write(source.path().join(".build/bridge.obj"), "generated").unwrap(); - fs::write(source.path().join("bridge.gd"), "source").unwrap(); - let sources = source_files(source.path()).unwrap(); - assert_eq!(sources.len(), 3); - assert!(sources.contains(&source.path().join("bridge.gd"))); - assert!(!sources.iter().any(|path| path - .components() - .any(|component| component.as_os_str() == "bin" || component.as_os_str() == ".build"))); - } } diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs b/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs index 05fae4480..636ba82be 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.generated.rs @@ -105,7 +105,7 @@ Subdirectory { origin: "prepared", target_contains: &["windows"], targets: &[], - features: &[], + features: &["cocos-editor-injection"], }, Subdirectory { path: "dotnet/publish/win-x64", diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.json b/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.json index 1a3d5401b..a6c8c04a4 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.json +++ b/apps/ai-game-creator-shell/src-tauri/build_support/package-layout.json @@ -121,7 +121,10 @@ "windows" ], "plugin": "agc-cocos-editor", - "prepare": "cocos-bridge-build" + "prepare": "cocos-bridge-build", + "features": [ + "cocos-editor-injection" + ] }, { "path": "dotnet/publish/win-x64", diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/package_layout.rs b/apps/ai-game-creator-shell/src-tauri/build_support/package_layout.rs index e75f787ab..8c8732039 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/package_layout.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/package_layout.rs @@ -866,8 +866,8 @@ mod tests { .filter(|entry| subdirectory_is_prepared(entry)) .map(|entry| entry.path) .collect::>(); - assert_eq!(source, ["src", "panels", "skills", "native/payload"]); - assert_eq!(prepared, ["dotnet/publish/win-x64"]); + assert_eq!(source, ["src", "panels", "skills"]); + assert_eq!(prepared, ["native/payload", "dotnet/publish/win-x64"]); } #[test]