按第三轮复测修复 M3:dry-run 零写入、所有权先行、指纹与门禁补口
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m23s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m1s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / Frontend tests (pull_request) Successful in 1m53s
Project CI / Native shell tests (pull_request) Successful in 5m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m51s
Project CI / Repository checks (pull_request) Successful in 1m57s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m26s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m39s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m23s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m1s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / Frontend tests (pull_request) Successful in 1m53s
Project CI / Native shell tests (pull_request) Successful in 5m46s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m51s
Project CI / Repository checks (pull_request) Successful in 1m57s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m26s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m39s
- 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)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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` → 两次快照必须逐项一致。
|
||||
|
||||
Reference in New Issue
Block a user