From 547fb0dbceee9bfef8f8271c1d19f9853c82d329 Mon Sep 17 00:00:00 2001 From: suzmii Date: Mon, 28 Sep 2026 01:03:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E7=AC=AC=E4=B8=89=E8=BD=AE=E5=A4=8D?= =?UTF-8?q?=E6=B5=8B=E4=BF=AE=E5=A4=8D=20M3=EF=BC=9Adry-run=20=E9=9B=B6?= =?UTF-8?q?=E5=86=99=E5=85=A5=E3=80=81=E6=89=80=E6=9C=89=E6=9D=83=E5=85=88?= =?UTF-8?q?=E8=A1=8C=E3=80=81=E6=8C=87=E7=BA=B9=E4=B8=8E=E9=97=A8=E7=A6=81?= =?UTF-8?q?=E8=A1=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P2:preparePlugins 重排——dry-run 只做只读判断即返回,预缓存交付不再排在 dry-run 之前;所有权断言提到任何写入之前 - P3:pluginSourceFingerprint 放开 origin=source 过滤(prepared 内容哈希分支可达),库文件与 payload 交付态纳入指纹,pluginTreeMatches 增加「不该存在却存在 → 需要重建」判定 - P3:门禁交叉校验 nativePayloads[].destinationSubdirectory 必须是同插件 origin=prepared 的子目录之一 - P2:里程碑验收清单注明 Cocos payload 只在 injection 构建交付并给出对应命令;dry-run 用例补 injection feature 覆盖 - 夹具补齐 agc-cocos-editor 插件(所有权检查要求目标插件真实存在) - 验证:工具用例 13/13、声明门禁通过 - 已知缺口:injection 构建后切回默认 feature,随包目录里陈旧 payload 的清理尚未生效(已记在 PR) --- .../scripts/check-package-layout.mjs | 30 ++++++ .../scripts/prepare-bundled-resources.mjs | 92 ++++++++++++++----- .../prepare-bundled-resources.test.mjs | 22 ++++- ...¼–辑器分支产物归位与症状层补丁清理-2026-09-26.md | 3 +- 4 files changed, 123 insertions(+), 24 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 f7d10f97d..90dd0ce92 100755 --- a/apps/ai-game-creator-shell/scripts/check-package-layout.mjs +++ b/apps/ai-game-creator-shell/scripts/check-package-layout.mjs @@ -536,6 +536,36 @@ function validateExtendedDeclarations() { fail(`${at}.prepare 引用了未声明的准备步骤:${payload.prepare}`); } } + const preparedByPlugin = new Map(); + for (const entry of expectArray( + plugins.subdirectories ?? [], + 'plugins.subdirectories', + )) { + const subdirectory = expectObject(entry, 'subdirectory'); + if (subdirectory.origin !== 'prepared') { + continue; + } + const owner = expectString( + subdirectory.plugin ?? '', + 'subdirectory.plugin', + ); + if (!owner) { + fail(`origin=prepared 的子目录必须声明 plugin:${subdirectory.path}`); + } + preparedByPlugin.set(owner, [ + ...(preparedByPlugin.get(owner) ?? []), + subdirectory.path, + ]); + } + for (const payload of payloads) { + const candidates = preparedByPlugin.get(payload.plugin) ?? []; + if (!candidates.includes(payload.destinationSubdirectory)) { + fail( + `nativePayloads 的 destinationSubdirectory(${payload.destinationSubdirectory})必须是该插件 origin=prepared 的子目录之一(现有:${candidates.join('、') || '无'})`, + ); + } + } + const referenced = [ ...expectArray(plugins.subdirectories ?? [], 'plugins.subdirectories') .filter( 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 2b5e5dae8..9ec3828ac 100755 --- a/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs +++ b/apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs @@ -659,10 +659,10 @@ function pluginSourceFingerprint(declaration, plugins, target, features) { const lines = []; for (const plugin of plugins) { for (const subdirectory of declaration.plugins.subdirectories) { - if (subdirectory.origin !== 'source') { - continue; - } - if (!subdirectoryEnabled(subdirectory, target, features)) { + if ( + subdirectoryAppliesToPlugin(subdirectory, plugin.name) || + subdirectoryEnabled(subdirectory, target, features) + ) { continue; } const root = path.join(plugin.root, subdirectory.path); @@ -685,6 +685,44 @@ function pluginSourceFingerprint(declaration, plugins, target, features) { lines.push( `${plugin.name}/plugin.json:${info.size}:${Math.round(info.mtimeMs)}`, ); + for (const staging of declaration.plugins.libraryStaging ?? []) { + if ( + plugin.name !== staging.plugin || + libraryStagingEnabled(staging, target, features) + ) { + continue; + } + for (const relative of staging.files) { + const file = path.join( + plugin.root, + staging.sourceSubdirectory, + relative, + ); + lines.push( + `${plugin.name}/${staging.sourceSubdirectory}/${relative}:${ + existsSync(file) ? `sha256:${sha256File(file)}` : 'missing' + }`, + ); + } + } + for (const payload of declaration.plugins.nativePayloads ?? []) { + if ( + plugin.name !== payload.plugin || + nativePayloadEnabled(payload, target, features) + ) { + continue; + } + const delivered = path.join( + plugin.root, + payload.destinationSubdirectory, + payload.destinationFileName ?? payload.sourceFileName, + ); + lines.push( + `${plugin.name}/${payload.destinationSubdirectory}:${ + existsSync(delivered) ? `sha256:${sha256File(delivered)}` : 'missing' + }`, + ); + } } return sha256Text(lines.join('\n')); } @@ -715,6 +753,14 @@ function pluginTreeMatches( ) { continue; } + const stagedDirectory = path.join(pluginDestination, subdirectory.path); + if (!subdirectoryEnabled(subdirectory, target, features)) { + // 当前目标/feature 下不该出现的子目录:存在即说明是别的构建留下的,必须重建清理。 + if (existsSync(stagedDirectory)) { + return false; + } + continue; + } const sourceRoot = path.join(plugin.root, subdirectory.path); if (subdirectory.origin !== 'source') { if ( @@ -1091,6 +1137,26 @@ function preparePlugins({ destinationRoot, declaration.plugins.destinationDirectory, ); + const fingerprint = pluginSourceFingerprint( + declaration, + plugins, + target, + features, + ); + const upToDate = + record.plugins?.fingerprint === fingerprint && + pluginTreeMatches(declaration, plugins, target, features, destination); + // dry-run 必须零写入:只做只读判断就返回。 + if (dryRun) { + return { + summary: upToDate + ? `plugins 命中缓存(未写入,${plugins.length} 个插件)` + : `plugins 需要重新生成(dry-run 未写入,${plugins.length} 个插件)`, + record: undefined, + }; + } + // 所有权断言先于任何写入(含下面的预缓存交付)。 + assertOwnedPluginRoot(destination, plugins); // payload 交付不能排在缓存短路之后:否则「先默认构建、之后开 injection」会把交付整条跳过。 if (existsSync(destination)) { copyNativePayloads({ @@ -1103,28 +1169,12 @@ function preparePlugins({ profile, }); } - const fingerprint = pluginSourceFingerprint( - declaration, - plugins, - target, - features, - ); - if ( - record.plugins?.fingerprint === fingerprint && - pluginTreeMatches(declaration, plugins, target, features, destination) - ) { + if (upToDate) { return { summary: `plugins 命中缓存(未写入,${plugins.length} 个插件)`, record: undefined, }; } - if (dryRun) { - return { - summary: `plugins 需要重新生成(dry-run 未写入,${plugins.length} 个插件)`, - record: undefined, - }; - } - assertOwnedPluginRoot(destination, plugins); stageAtomically(destination, (staging) => { for (const plugin of plugins) { const pluginDestination = path.join(staging, plugin.name); 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 2bb540151..0a161a4ad 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 @@ -139,6 +139,17 @@ function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) { fs.writeFileSync(path.join(pluginRoot, 'target/junk.rs'), 'junk\n'); fs.writeFileSync(path.join(pluginRoot, '.git/HEAD'), 'ref\n'); fs.writeFileSync(path.join(pluginRoot, '.env'), 'secret\n'); + + const cocosRoot = path.join(repoRoot, 'plugins/agc-cocos-editor'); + fs.mkdirSync(path.join(cocosRoot, 'src'), { recursive: true }); + fs.writeFileSync( + path.join(cocosRoot, 'plugin.json'), + '{"name":"agc-cocos-editor"}\n', + ); + fs.writeFileSync( + path.join(cocosRoot, 'src/entry.mjs'), + 'export const cocos = 1;\n', + ); } writePrepareArtifacts(repoRoot, declaration); @@ -455,7 +466,14 @@ test('fails closed when the destination is owned by something else', () => { test('dry run writes nothing', () => { const fixture = buildFixture(); try { - const summaries = prepare(fixture, { dryRun: true }); + const summaries = prepare(fixture, { + dryRun: true, + features: new Set([ + 'unity-editor-execute', + 'godot-editor-execute', + 'cocos-editor-injection', + ]), + }); assert.match(summaries[0], /需要重新生成(dry-run 未写入)/); const unit = path.join(fixture.destinationRoot, 'resources/codex/win-x64'); assert.deepEqual( @@ -497,7 +515,7 @@ test('declaration drives source lookup and staging units', () => { source, /codex-win32-x64[\\/]vendor[\\/]x86_64-pc-windows-msvc$/u, ); - assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 1); + assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 2); } finally { fixture.cleanup(); } diff --git a/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md b/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md index d9be7289d..8c0ca00b1 100644 --- a/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md +++ b/docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md @@ -51,7 +51,8 @@ 1. **准备步骤单独跑(不打包)** - `npm run agc:bundled-resources:prepare -- --target x86_64-pc-windows-msvc` - - 预期:一行汇总日志;`src-tauri/resources/plugins/agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe`、`agc-godot-editor/native/gdextension/` 下的扩展、`agc-cocos-editor/native/payload/cocos-editor-bridge.dll` 全部在位。 + - 预期:一行汇总日志;`src-tauri/resources/plugins/agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe`、`agc-godot-editor/native/gdextension/` 下的扩展在位。 + - Cocos payload 只在 injection 构建下交付(与迁移前一致):加 `--features=cocos-editor-execute,unity-editor-execute,godot-editor-execute,cocos-editor-injection` 再跑一次,确认 `agc-cocos-editor/native/payload/cocos-editor-bridge.dll` 同时出现在插件工作区与随包目录。 - 再跑一次:预期全部「命中缓存」,且 `resources/**` 的文件时间戳不变。 2. **构建期不再写随包资源** - 取 `src-tauri/resources` 全量快照(相对路径/大小/mtime/sha256)→ `touch apps/ai-game-creator-shell/src-tauri/build.rs` → 再 `cargo build` → 两次快照必须逐项一致。