按复测修复 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
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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user