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 617257d34..a8eda97e1 100755 --- a/apps/ai-game-creator-shell/scripts/check-package-layout.mjs +++ b/apps/ai-game-creator-shell/scripts/check-package-layout.mjs @@ -216,6 +216,10 @@ function parseDeclaration(raw) { return { path: expectString(parsed.path, `${at}.path`), origin: expectOrigin(parsed.origin, `${at}.origin`), + plugin: + parsed.plugin === undefined + ? '' + : expectString(parsed.plugin, `${at}.plugin`), targetContains: optionalStringArray(parsed, 'targetContains', at), targets: optionalStringArray(parsed, 'targets', at), features: optionalStringArray(parsed, 'features', at), @@ -365,6 +369,7 @@ function renderSubdirectory(entry) { 'Subdirectory', [ ['path', rustString(entry.path)], + ['plugin', rustString(entry.plugin ?? '')], ['origin', rustString(entry.origin)], ['target_contains', rustStrings(entry.targetContains)], ['targets', rustStrings(entry.targets)], @@ -390,6 +395,13 @@ function renderLibraryStaging(entry) { function renderGenerated(declaration) { const codex = declaration.codex; + const godotStaging = declaration.plugins.libraryStaging.find( + (entry) => entry.layout === 'godot-bundle', + ); + if (!godotStaging || godotStaging.files.length === 0) { + fail('声明缺少 godot-bundle 随包文件清单,拒绝生成空清单'); + } + const godotFiles = godotStaging.files; const plugins = declaration.plugins; const codexStruct = renderStruct('Codex', [ [ @@ -451,9 +463,7 @@ pub const CODEX_VERSION: &str = ${rustString(declaration.codex.version)}; pub const CODEX_CLI_VERSION: &str = ${rustString(declaration.codex.cliVersionPrefix + declaration.codex.version)}; pub const CODEX_MANIFEST_SCHEMA: &str = ${rustString(declaration.codex.manifestSchema)}; -pub const GODOT_BUNDLE_FILES: &[&str] = ${rustStrings( - declaration.plugins.libraryStaging[0]?.files ?? [], - )}; +pub const GODOT_BUNDLE_FILES: &[&str] = ${rustStrings(godotFiles)}; pub const CODEX: Codex = ${codexStruct}; 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 577e638c1..9b6a9508b 100755 --- a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs @@ -572,6 +572,10 @@ export function pluginDirectories(declaration, repoRoot) { ); } +function subdirectoryAppliesToPlugin(subdirectory, pluginName) { + return !subdirectory.plugin || subdirectory.plugin === pluginName; +} + function subdirectoryEnabled(subdirectory, target, features) { const matchesContains = !subdirectory.targetContains?.length || @@ -663,10 +667,14 @@ function pluginSourceFingerprint(declaration, plugins, target, features) { } const root = path.join(plugin.root, subdirectory.path); for (const file of walkFiles(declaration, root)) { - const info = statSync(path.join(root, file)); - lines.push( - `${plugin.name}/${subdirectory.path}/${file}:${info.size}:${Math.round(info.mtimeMs)}`, - ); + const absolute = path.join(root, file); + const info = statSync(absolute); + // prepared 产物由准备步骤无条件复制,mtime 会变;用内容哈希保证幂等判断稳定。 + const stamp = + subdirectory.origin === 'prepared' + ? `sha256:${sha256File(absolute)}` + : `${info.size}:${Math.round(info.mtimeMs)}`; + lines.push(`${plugin.name}/${subdirectory.path}/${file}:${stamp}`); } } const manifest = path.join( @@ -701,10 +709,22 @@ function pluginTreeMatches( return false; } for (const subdirectory of declaration.plugins.subdirectories) { - if (!subdirectoryEnabled(subdirectory, target, features)) { + if ( + subdirectoryAppliesToPlugin(subdirectory, plugin.name) || + subdirectoryEnabled(subdirectory, target, features) + ) { continue; } const sourceRoot = path.join(plugin.root, subdirectory.path); + if (subdirectory.origin !== 'source') { + if ( + existsSync(sourceRoot) && + !existsSync(path.join(pluginDestination, subdirectory.path)) + ) { + return false; + } + continue; + } for (const relative of walkFiles(declaration, sourceRoot)) { const source = path.join(sourceRoot, relative); const staged = path.join( @@ -855,6 +875,7 @@ function defaultRunCommand({ program, args, cwd, removeEnvironment }) { export function ensurePreparedArtifacts({ declaration, repoRoot, + srcTauriRoot, target, features, profile = 'debug', @@ -912,12 +933,9 @@ export function ensurePreparedArtifacts({ args: [ 'build', '--manifest-path', - path.join( - repoRoot, - 'apps/ai-game-creator-shell/src-tauri/Cargo.toml', - ), - '-p', - path.basename(step.packageDirectory), + path.join(repoRoot, step.packageDirectory, 'Cargo.toml'), + '--target-dir', + path.join(srcTauriRoot, 'target'), '--target', target, ...(profile === 'release' ? ['--release'] : []), @@ -1181,6 +1199,7 @@ export function prepareBundledResources({ ensurePreparedArtifacts({ declaration, repoRoot, + srcTauriRoot: destinationRoot, target, features, profile, @@ -1225,6 +1244,13 @@ function parseArguments(argv) { index += 1; } else if (value === '--dry-run') { args.dryRun = true; + } else if (value === '--features') { + args.features = new Set( + String(argv[index + 1] ?? '') + .split(',') + .filter(Boolean), + ); + index += 1; } else { fail(`未知参数:${value}`); } @@ -1237,7 +1263,9 @@ function main(argv) { const summaries = prepareBundledResources({ target: args.target ?? resolveHostTarget(), destinationRoot: args.destinationRoot ?? SRC_TAURI_DIR, + ...(args.features ? { features: args.features } : {}), dryRun: args.dryRun, + log: (line) => console.log(`[agc-resources] ${line}`), }); for (const summary of summaries) { console.log(`[agc-resources] ${summary}`); diff --git a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.test.mjs b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.test.mjs index 19b90da94..2bb540151 100644 --- a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.test.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.test.mjs @@ -148,7 +148,7 @@ function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) { WINDOWS_TARGET, 'debug', 'deps', - 'cocos-editor-bridge.dll', + 'cocos_editor_bridge.dll', ); fs.mkdirSync(path.dirname(cocosDll), { recursive: true }); fs.writeFileSync(cocosDll, 'cocos bridge payload\n'); @@ -493,7 +493,10 @@ test('declaration drives source lookup and staging units', () => { app: fixture.appRoot, repo: fixture.repoRoot, }); - assert.match(source, /codex-win32-x64\/vendor\/x86_64-pc-windows-msvc$/); + assert.match( + source, + /codex-win32-x64[\\/]vendor[\\/]x86_64-pc-windows-msvc$/u, + ); assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 1); } finally { fixture.cleanup(); diff --git a/apps/ai-game-creator-shell/src-tauri/build.rs b/apps/ai-game-creator-shell/src-tauri/build.rs index 3138d2279..0e98da9a4 100644 --- a/apps/ai-game-creator-shell/src-tauri/build.rs +++ b/apps/ai-game-creator-shell/src-tauri/build.rs @@ -99,6 +99,7 @@ fn validate_prepared_payloads(manifest_dir: &std::path::Path, target: &str) { { for subdirectory in declared.subdirectories { if !package_layout::subdirectory_is_prepared(subdirectory) + || !package_layout::subdirectory_applies_to_plugin(subdirectory, &plugin.name) || !package_layout::subdirectory_enabled( subdirectory, target, 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 88247e41d..05fae4480 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 @@ -77,6 +77,7 @@ pub const PLUGINS: Plugins = Plugins { subdirectories: &[ Subdirectory { path: "src", + plugin: "", origin: "source", target_contains: &[], targets: &[], @@ -84,6 +85,7 @@ Subdirectory { }, Subdirectory { path: "panels", + plugin: "", origin: "source", target_contains: &[], targets: &[], @@ -91,6 +93,7 @@ Subdirectory { }, Subdirectory { path: "skills", + plugin: "", origin: "source", target_contains: &[], targets: &[], @@ -98,13 +101,15 @@ Subdirectory { }, Subdirectory { path: "native/payload", - origin: "source", + plugin: "agc-cocos-editor", + origin: "prepared", target_contains: &["windows"], targets: &[], features: &[], }, Subdirectory { path: "dotnet/publish/win-x64", + plugin: "agc-unity-editor", origin: "prepared", target_contains: &[], targets: &["x86_64-pc-windows-msvc"], 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 8164358ed..1a3d5401b 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 @@ -15,19 +15,27 @@ "manifestFileName": "manifest.json", "packageMetadataFileName": "codex-package.json", "noticeFileName": "NOTICE.md", - "sourceRoots": ["app", "repo"], + "sourceRoots": [ + "app", + "repo" + ], "sourceRelativePaths": [ "node_modules/@openai/codex-/vendor/", "node_modules/@openai/codex/node_modules/@openai/codex-/vendor/" ], "noticeSources": [ { - "targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"], + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin" + ], "source": "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md", "preserve": false }, { - "targets": ["x86_64-pc-windows-msvc"], + "targets": [ + "x86_64-pc-windows-msvc" + ], "source": "resources/codex/win-x64/NOTICE.md", "preserve": true } @@ -36,7 +44,10 @@ { "name": "mac-native", "directory": "mac-native", - "targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"] + "targets": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin" + ] } ], "targets": [ @@ -86,18 +97,43 @@ "sourceDirectory": "plugins", "destinationDirectory": "resources/plugins", "manifestFileName": "plugin.json", - "targetContainsAny": ["windows", "apple-darwin"], + "targetContainsAny": [ + "windows", + "apple-darwin" + ], "subdirectories": [ - { "path": "src", "origin": "source" }, - { "path": "panels", "origin": "source" }, - { "path": "skills", "origin": "source" }, - { "path": "native/payload", "origin": "source", "targetContains": ["windows"] }, + { + "path": "src", + "origin": "source" + }, + { + "path": "panels", + "origin": "source" + }, + { + "path": "skills", + "origin": "source" + }, + { + "path": "native/payload", + "origin": "prepared", + "targetContains": [ + "windows" + ], + "plugin": "agc-cocos-editor", + "prepare": "cocos-bridge-build" + }, { "path": "dotnet/publish/win-x64", "origin": "prepared", "prepare": "unity-helper-publish", - "targets": ["x86_64-pc-windows-msvc"], - "features": ["unity-editor-execute"] + "targets": [ + "x86_64-pc-windows-msvc" + ], + "features": [ + "unity-editor-execute" + ], + "plugin": "agc-unity-editor" } ], "libraryStaging": [ @@ -105,8 +141,12 @@ "plugin": "agc-godot-editor", "sourceSubdirectory": "native/gdextension", "prepare": "godot-extension-build", - "targets": ["x86_64-pc-windows-msvc"], - "features": ["godot-editor-execute"], + "targets": [ + "x86_64-pc-windows-msvc" + ], + "features": [ + "godot-editor-execute" + ], "layout": "godot-bundle", "files": [ "bin/win-x64/agc_godot_editor.dll", @@ -120,10 +160,15 @@ { "plugin": "agc-cocos-editor", "prepare": "cocos-bridge-build", - "sourceFileName": "cocos-editor-bridge.dll", + "sourceFileName": "cocos_editor_bridge.dll", "destinationSubdirectory": "native/payload", - "targets": ["x86_64-pc-windows-msvc"], - "features": ["cocos-editor-injection"] + "targets": [ + "x86_64-pc-windows-msvc" + ], + "features": [ + "cocos-editor-injection" + ], + "destinationFileName": "cocos-editor-bridge.dll" } ], "prepareSteps": [ @@ -133,8 +178,15 @@ "workingDirectory": "plugins/agc-unity-editor/dotnet", "scriptFileName": "build.ps1", "fingerprint": { - "roots": ["."], - "excludeDirectoryNames": ["bin", "obj", "publish", "native-build"], + "roots": [ + "." + ], + "excludeDirectoryNames": [ + "bin", + "obj", + "publish", + "native-build" + ], "stampRelativePath": "publish/win-x64/.agc-source.sha256" }, "requiredOutputs": [ @@ -155,25 +207,49 @@ "kind": "powershell", "workingDirectory": "plugins/agc-godot-editor/native/gdextension", "scriptFileName": "build.ps1", - "removeEnvironment": ["PSModulePath"], + "removeEnvironment": [ + "PSModulePath" + ], "requiredOutputs": [ "plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll", "plugins/agc-godot-editor/native/gdextension/bin/win-x64/metadata.json", "plugins/agc-godot-editor/native/gdextension/vendor/LICENSE.txt", "plugins/agc-godot-editor/native/gdextension/vendor/provenance.json" - ] + ], + "fingerprint": { + "roots": [ + "." + ], + "excludeDirectoryNames": [ + "bin", + ".build", + "native-build" + ], + "stampRelativePath": "bin/win-x64/.agc-source.sha256" + } }, { "name": "cocos-bridge-build", "kind": "cargo", "packageDirectory": "plugins/agc-cocos-editor/native/cocos-editor-bridge", - "features": ["windows-injection"], + "features": [ + "windows-injection" + ], "requiredOutputs": [] } ], - "skipDirectoryNames": ["target", "node_modules"], - "skipDirectoryNamePrefixes": ["."], - "skipFileNamePrefixes": ["."], - "skipFileNameFragments": [".test."] + "skipDirectoryNames": [ + "target", + "node_modules" + ], + "skipDirectoryNamePrefixes": [ + "." + ], + "skipFileNamePrefixes": [ + "." + ], + "skipFileNameFragments": [ + ".test." + ] } } 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 e27c8bedd..e75f787ab 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 @@ -49,6 +49,8 @@ pub struct PackageMetadata { #[derive(Debug)] pub struct Subdirectory { pub path: &'static str, + /// 该子目录归属的插件名;空串表示适用于所有插件。 + pub plugin: &'static str, /// `source`:由声明与仓库源码就能生成(准备步骤负责);`build`:构建期工具链产出(构建脚本负责)。 pub origin: &'static str, pub target_contains: &'static [&'static str], @@ -171,6 +173,11 @@ pub fn plugin_staging_applies(target: &str) -> bool { .any(|needle| target.contains(needle)) } +/// 子目录是否归属该插件(空串表示通用)。 +pub fn subdirectory_applies_to_plugin(subdirectory: &Subdirectory, plugin_name: &str) -> bool { + subdirectory.plugin.is_empty() || subdirectory.plugin == plugin_name +} + /// 子目录在当前目标与已启用 feature 下是否随包。 pub fn subdirectory_enabled( subdirectory: &Subdirectory, diff --git a/docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md b/docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md index 245d7cfce..2f5d10de7 100644 --- a/docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md +++ b/docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md @@ -156,7 +156,7 @@ build script 只写 `OUT_DIR`/`target`;随包资源是它的输入。凡需要 形态选择**混合**:生成归 Node(复用 `stage-node-runtime.mjs` / `prepare-macos-codex.mjs` 的下载、`integrity`、临时目录 + rename 原子替换范式),声明与校验归 Rust(复用 `codex_bundle.rs` / `godot_bundle.rs` 的布局与摘要校验,运行期模块不改公开接口)。理由:Rust 侧没有下载与 lockfile 解析能力(`[build-dependencies]` 无 HTTP 客户端),Node 侧没有 staging 能力;任选单一语言都要迁移另一侧既有资产。Rust 侧刻意不解析 JSON:声明经生成器变成编译期常量,避免运行期解析与生命周期妥协,也让 `&'static` 布局表与现有调用点保持不变。 -准备步骤的验证入口:`AGC_SKIP_RESOURCE_STAGING=1 cargo build/check …` 只跑只读校验、跳过写入分支,用于在既有产物上单独验证校验路径。 +构建脚本自 M3 起只做只读校验(写入分支与 `AGC_SKIP_RESOURCE_STAGING` 开关一并删除),`cargo build/check` 本身就是对既有产物的校验。 ### 4.9 M2/M3 实况:构建期写入边界