按复测修复 M3:payload 门槛与交付、缓存键、条件写反与门禁覆盖
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Backend tests (pull_request) Successful in 3m50s
Project CI / Frontend tests (pull_request) Successful in 1m58s
Project CI / Native shell tests (pull_request) Successful in 5m55s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m12s
Project CI / Repository checks (pull_request) Successful in 1m51s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m24s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m51s

- 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 通过
This commit is contained in:
2026-09-28 00:50:48 +08:00
parent e4634928c6
commit 06ffa7b6ac
6 changed files with 104 additions and 122 deletions
@@ -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(
@@ -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] ?? '')
@@ -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<Vec<(&'static str, Vec<u8>)>, 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<Vec<PathBuf>, 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")));
}
}
@@ -105,7 +105,7 @@ Subdirectory {
origin: "prepared",
target_contains: &["windows"],
targets: &[],
features: &[],
features: &["cocos-editor-injection"],
},
Subdirectory {
path: "dotnet/publish/win-x64",
@@ -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",
@@ -866,8 +866,8 @@ mod tests {
.filter(|entry| subdirectory_is_prepared(entry))
.map(|entry| entry.path)
.collect::<Vec<_>>();
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]