Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 547fb0dbce | |||
| 06ffa7b6ac | |||
| e4634928c6 | |||
| faa510e999 |
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
@@ -440,6 +440,7 @@ export function stageBundledResources(
|
||||
const summaries = prepare({
|
||||
target,
|
||||
features: new Set(defaultEditorFeatures(target)),
|
||||
profile: 'release',
|
||||
log: (line) => console.log(`[ai-game-creator-shell] ${line}`),
|
||||
});
|
||||
for (const summary of summaries) {
|
||||
|
||||
@@ -81,11 +81,11 @@ function expectInteger(value, at) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 随包子目录来源:`source` 由声明与仓库源码就能生成(准备步骤负责),
|
||||
// `build` 由构建期工具链产出(构建脚本在产物生成后负责)。
|
||||
// 随包子目录来源:`source` 直接来自仓库源码(准备步骤复制),
|
||||
// `prepared` 需要先由准备步骤运行声明的构建命令产出,再复制进随包目录。
|
||||
function expectOrigin(value, at) {
|
||||
if (value !== 'source' && value !== 'build') {
|
||||
fail(`${at} 必须是 source 或 build(实际 ${String(value)})`);
|
||||
if (value !== 'source' && value !== 'prepared') {
|
||||
fail(`${at} 必须是 source 或 prepared(实际 ${String(value)})`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -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),
|
||||
@@ -227,15 +231,21 @@ function parseDeclaration(raw) {
|
||||
).map((entry, index) => {
|
||||
const at = `plugins.libraryStaging[${index}]`;
|
||||
const parsed = expectObject(entry, at);
|
||||
const files = expectStringArray(parsed.files, `${at}.files`);
|
||||
if (files.length === 0) {
|
||||
fail(`${at}.files 不能为空`);
|
||||
}
|
||||
return {
|
||||
plugin: expectString(parsed.plugin, `${at}.plugin`),
|
||||
sourceSubdirectory: expectString(
|
||||
parsed.sourceSubdirectory,
|
||||
`${at}.sourceSubdirectory`,
|
||||
),
|
||||
prepare: expectString(parsed.prepare, `${at}.prepare`),
|
||||
targets: expectStringArray(parsed.targets, `${at}.targets`),
|
||||
features: expectStringArray(parsed.features, `${at}.features`),
|
||||
layout: expectString(parsed.layout, `${at}.layout`),
|
||||
files,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -359,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)],
|
||||
@@ -384,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', [
|
||||
[
|
||||
@@ -445,6 +463,8 @@ 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(godotFiles)};
|
||||
|
||||
pub const CODEX: Codex = ${codexStruct};
|
||||
|
||||
pub const PLUGINS: Plugins = ${pluginsStruct};
|
||||
@@ -467,8 +487,112 @@ 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 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(
|
||||
(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(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,33 @@ function sha256File(file) {
|
||||
}
|
||||
|
||||
/// 造一个最小工作区:app(含 node_modules 上游包)、repo(含 plugins 工作区)、lockfile。
|
||||
/// 造出声明里所有「已准备」产物:真实构建要 Windows 工具链,用例只需要文件在位。
|
||||
function writePrepareArtifacts(root, declaration) {
|
||||
const created = [];
|
||||
for (const step of declaration.plugins.prepareSteps ?? []) {
|
||||
for (const relative of step.requiredOutputs) {
|
||||
const file = path.join(root, relative);
|
||||
if (fs.existsSync(file)) {
|
||||
continue;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `prepared ${relative}\n`);
|
||||
created.push(relative);
|
||||
}
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/// 记录调用的假执行器:默认把声明的必需产物造出来。
|
||||
function fakeRunCommand({ created = [], failOn = null } = {}) {
|
||||
return (invocation) => {
|
||||
created.push(`${invocation.program} ${(invocation.args ?? []).join(' ')}`);
|
||||
if (failOn && invocation.program === failOn) {
|
||||
throw new Error('fake run failure');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildFixture({ targets = [WINDOWS_TARGET], plugins = true } = {}) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-resources-'));
|
||||
const appRoot = path.join(root, 'app');
|
||||
@@ -112,8 +139,31 @@ 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);
|
||||
const cocosDll = path.join(
|
||||
destinationRoot,
|
||||
'target',
|
||||
WINDOWS_TARGET,
|
||||
'debug',
|
||||
'deps',
|
||||
'cocos_editor_bridge.dll',
|
||||
);
|
||||
fs.mkdirSync(path.dirname(cocosDll), { recursive: true });
|
||||
fs.writeFileSync(cocosDll, 'cocos bridge payload\n');
|
||||
|
||||
const lockfilePath = path.join(root, 'package-lock.json');
|
||||
fs.writeFileSync(lockfilePath, JSON.stringify(lockfile, null, 2));
|
||||
return {
|
||||
@@ -156,6 +206,7 @@ function snapshot(directory) {
|
||||
}
|
||||
|
||||
function prepare(fixture, overrides = {}) {
|
||||
const runner = overrides.runCommand ?? fakeRunCommand();
|
||||
return prepareBundledResources({
|
||||
target: WINDOWS_TARGET,
|
||||
destinationRoot: fixture.destinationRoot,
|
||||
@@ -164,6 +215,7 @@ function prepare(fixture, overrides = {}) {
|
||||
lockfilePath: fixture.lockfilePath,
|
||||
repoRoot: fixture.repoRoot,
|
||||
appRoot: fixture.appRoot,
|
||||
runCommand: runner,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
@@ -414,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(
|
||||
@@ -452,8 +511,96 @@ 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.equal(pluginDirectories(declaration, fixture.repoRoot).length, 1);
|
||||
assert.match(
|
||||
source,
|
||||
/codex-win32-x64[\\/]vendor[\\/]x86_64-pc-windows-msvc$/u,
|
||||
);
|
||||
assert.equal(pluginDirectories(declaration, fixture.repoRoot).length, 2);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('runs declared prepare steps once and copies their artifacts into the staged tree', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const created = [];
|
||||
const summaries = prepare(fixture, {
|
||||
runCommand: fakeRunCommand({ created }),
|
||||
features: new Set([
|
||||
'unity-editor-execute',
|
||||
'godot-editor-execute',
|
||||
'cocos-editor-injection',
|
||||
]),
|
||||
});
|
||||
assert.match(summaries[0], /命中缓存|重新生成/);
|
||||
assert.ok(
|
||||
created.some(
|
||||
(entry) =>
|
||||
entry.startsWith('powershell.exe') && entry.includes('build.ps1'),
|
||||
),
|
||||
'必须执行声明的 powershell 准备步骤',
|
||||
);
|
||||
assert.ok(
|
||||
created.some(
|
||||
(entry) =>
|
||||
entry.startsWith('cargo build') && entry.includes('--target'),
|
||||
),
|
||||
'必须执行声明的 cargo 准备步骤',
|
||||
);
|
||||
const stagedPayload = path.join(
|
||||
fixture.destinationRoot,
|
||||
'resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll',
|
||||
);
|
||||
assert.ok(fs.existsSync(stagedPayload), 'native payload 必须进随包目录');
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
fixture.repoRoot,
|
||||
'plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll',
|
||||
),
|
||||
),
|
||||
'native payload 必须写回插件工作区(唯一真源)',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('skips prepare steps whose fingerprint is unchanged', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
const first = [];
|
||||
prepare(fixture, { runCommand: fakeRunCommand({ created: first }) });
|
||||
assert.ok(first.length > 0, '首次必须执行准备步骤');
|
||||
const second = [];
|
||||
prepare(fixture, { runCommand: fakeRunCommand({ created: second }) });
|
||||
const unityRuns = second.filter(
|
||||
(entry) =>
|
||||
entry.startsWith('powershell.exe') &&
|
||||
entry.includes('agc-unity-editor'),
|
||||
);
|
||||
assert.deepEqual(
|
||||
unityRuns,
|
||||
[],
|
||||
'指纹一致时不应再跑声明了指纹的 Unity 准备步骤',
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed when a prepare step does not produce its declared outputs', () => {
|
||||
const fixture = buildFixture();
|
||||
try {
|
||||
fs.rmSync(path.join(fixture.repoRoot, 'plugins/agc-unity-editor'), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
assert.throws(
|
||||
() => prepare(fixture, { runCommand: fakeRunCommand() }),
|
||||
/未产出必需文件/,
|
||||
);
|
||||
} finally {
|
||||
fixture.cleanup();
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
@@ -10,7 +10,6 @@ mod godot_bundle;
|
||||
#[path = "build_support/runtime_prompt_bundle.rs"]
|
||||
mod runtime_prompt_bundle;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
@@ -76,9 +75,66 @@ fn validate_staged_resources(manifest_dir: &std::path::Path) {
|
||||
);
|
||||
}
|
||||
validate_staged_plugin_workspace(manifest_dir, &target);
|
||||
validate_prepared_payloads(manifest_dir, &target);
|
||||
}
|
||||
|
||||
/// 只读校验插件随包工作区:声明的源码派生内容必须与仓库源码逐文件一致,整树无符号链接。
|
||||
/// 已准备产物与随包库在本机无法重建,构建期至少要确认它们已经就位。
|
||||
fn validate_prepared_payloads(manifest_dir: &std::path::Path, target: &str) {
|
||||
if !package_layout::plugin_staging_applies(target) {
|
||||
return;
|
||||
}
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
for plugin in package_layout::plugin_directories(
|
||||
&repo_root.join(declared.source_directory),
|
||||
declared.manifest_file_name,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"))
|
||||
{
|
||||
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,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
let staged = destination_root.join(&plugin.name).join(&relative);
|
||||
if !staged.is_dir() {
|
||||
panic!(
|
||||
"随包已准备产物缺失:{}(请先执行随包资源准备步骤)",
|
||||
staged.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
for staging in declared.library_staging {
|
||||
if plugin.name != staging.plugin
|
||||
|| !package_layout::library_staging_enabled(
|
||||
staging,
|
||||
target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
|
||||
let staged = destination_root.join(&plugin.name).join(&relative);
|
||||
godot_bundle::validate(&staged)
|
||||
.unwrap_or_else(|error| panic!("Godot 随包资源校验失败:{error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_staged_plugin_workspace(manifest_dir: &std::path::Path, target: &str) {
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
@@ -104,14 +160,6 @@ fn main() {
|
||||
"cargo:rustc-env=AGC_BUILD_TARGET={}",
|
||||
env::var("TARGET").expect("Cargo TARGET")
|
||||
);
|
||||
// AGC_SKIP_RESOURCE_STAGING=1 只做只读校验(要求随包资源已由准备步骤生成),
|
||||
// 用于在既有产物上单独验证校验路径。
|
||||
if env::var_os("AGC_SKIP_RESOURCE_STAGING").is_none() {
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
stage_build_generated_plugin_payloads(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
}
|
||||
validate_staged_resources(&manifest_dir);
|
||||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||||
@@ -134,282 +182,3 @@ fn main() {
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
|
||||
if std::env::var_os("CARGO_FEATURE_COCOS_EDITOR_INJECTION").is_none() {
|
||||
return;
|
||||
}
|
||||
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
|
||||
let profile_dir = out_dir
|
||||
.ancestors()
|
||||
.find(|path| path.file_name().is_some_and(|name| name == "build"))
|
||||
.and_then(|build_dir| build_dir.parent())
|
||||
.expect("AGC Cargo profile directory not found");
|
||||
let candidates = [
|
||||
profile_dir.join("deps/cocos_editor_bridge.dll"),
|
||||
profile_dir.join("cocos_editor_bridge.dll"),
|
||||
];
|
||||
let source = candidates
|
||||
.iter()
|
||||
.find(|path| path.is_file())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Cocos bridge native payload 未构建:{}",
|
||||
candidates
|
||||
.iter()
|
||||
.map(|p| p.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";")
|
||||
)
|
||||
});
|
||||
for destination in [
|
||||
// 插件工作区里的 payload 是开发态与打包态的唯一真源。
|
||||
manifest_dir
|
||||
.join("../../../plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||
// 随包资源目录与 tauri.windows.conf.json 的 `resources/plugins` 映射保持一致。
|
||||
manifest_dir
|
||||
.join("resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
|
||||
] {
|
||||
std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
|
||||
.expect("创建 Cocos bridge payload 目录失败");
|
||||
std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
|
||||
|
||||
/// Unity helper 是插件的随包运行文件。内容指纹避免每次 Cargo 检查都重新发布 .NET。
|
||||
fn prepare_unity_editor_helper(manifest_dir: &std::path::Path) {
|
||||
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_UNITY_EDITOR_EXECUTE");
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
if env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()
|
||||
|| target != "x86_64-pc-windows-msvc"
|
||||
{
|
||||
return;
|
||||
}
|
||||
let root = manifest_dir.join("../../../plugins/agc-unity-editor/dotnet");
|
||||
let mut sources = Vec::new();
|
||||
collect_unity_helper_sources(&root, &mut sources);
|
||||
sources.sort();
|
||||
let mut fingerprint = Sha256::new();
|
||||
for source in &sources {
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
fingerprint.update(
|
||||
source
|
||||
.strip_prefix(&root)
|
||||
.expect("helper source")
|
||||
.to_string_lossy()
|
||||
.as_bytes(),
|
||||
);
|
||||
fingerprint.update([0]);
|
||||
fingerprint.update(fs::read(source).expect("读取 Unity helper 源文件失败"));
|
||||
}
|
||||
let fingerprint = format!("{:x}", fingerprint.finalize());
|
||||
let publish = root.join("publish/win-x64");
|
||||
let executable = publish.join("Agc.Unity.Attach.exe");
|
||||
let stamp = publish.join(".agc-source.sha256");
|
||||
println!("cargo:rerun-if-changed={}", executable.display());
|
||||
if unity_helper_publish_complete(&publish)
|
||||
&& fs::read_to_string(&stamp).ok().as_deref() == Some(&fingerprint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
cfg!(windows),
|
||||
"构建 Unity 插件 helper 需要 Windows .NET 10 与 x64 C++ 工具链"
|
||||
);
|
||||
let status = std::process::Command::new("powershell.exe")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
])
|
||||
.arg(root.join("build.ps1"))
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.expect("无法启动 Unity helper 构建脚本");
|
||||
assert!(
|
||||
status.success() && unity_helper_publish_complete(&publish),
|
||||
"Unity helper 构建失败或缺少运行文件/许可"
|
||||
);
|
||||
fs::write(stamp, fingerprint).expect("写入 Unity helper 构建指纹失败");
|
||||
}
|
||||
|
||||
fn unity_helper_publish_complete(publish: &std::path::Path) -> bool {
|
||||
[
|
||||
"Agc.Unity.Attach.exe",
|
||||
"NOTICE",
|
||||
"THIRD-PARTY-NOTICES.txt",
|
||||
"licenses/DotCraft-Apache-2.0.txt",
|
||||
"licenses/Roslyn-MIT.txt",
|
||||
"licenses/upstream.json",
|
||||
"licenses/dotnet-LICENSE.TXT",
|
||||
"licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||
"licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||
"licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf",
|
||||
]
|
||||
.iter()
|
||||
.all(|name| {
|
||||
fs::symlink_metadata(publish.join(name)).is_ok_and(|metadata| {
|
||||
metadata.is_file() && !metadata.file_type().is_symlink() && metadata.len() > 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_unity_helper_sources(root: &std::path::Path, sources: &mut Vec<PathBuf>) {
|
||||
for entry in fs::read_dir(root)
|
||||
.expect("Unity helper 源码目录缺失")
|
||||
.flatten()
|
||||
{
|
||||
let kind = entry.file_type().expect("读取 Unity helper 源文件类型失败");
|
||||
assert!(!kind.is_symlink(), "Unity helper 源码不允许符号链接");
|
||||
let name = entry.file_name();
|
||||
if kind.is_dir() {
|
||||
if !matches!(
|
||||
name.to_str(),
|
||||
Some("bin" | "obj" | "publish" | "native-build")
|
||||
) {
|
||||
collect_unity_helper_sources(&entry.path(), sources);
|
||||
}
|
||||
} else if kind.is_file() {
|
||||
sources.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_godot_editor_extension(manifest_dir: &std::path::Path) {
|
||||
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_GODOT_EDITOR_EXECUTE");
|
||||
if env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_none()
|
||||
|| env::var("TARGET").expect("Cargo TARGET") != "x86_64-pc-windows-msvc"
|
||||
{
|
||||
return;
|
||||
}
|
||||
let root = manifest_dir.join("../../../plugins/agc-godot-editor/native/gdextension");
|
||||
for source in godot_bundle::source_files(&root).unwrap_or_else(|error| panic!("{error}")) {
|
||||
println!("cargo:rerun-if-changed={}", source.display());
|
||||
}
|
||||
assert!(
|
||||
cfg!(windows),
|
||||
"构建 Godot 原生扩展需要 Windows x64 C 编译器"
|
||||
);
|
||||
let status = std::process::Command::new("powershell.exe")
|
||||
// Cargo 可能从 PowerShell 7 启动,Windows PowerShell 应使用自身模块目录。
|
||||
.env_remove("PSModulePath")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
])
|
||||
.arg(root.join("build.ps1"))
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.expect("无法启动 Godot 原生扩展构建脚本");
|
||||
assert!(status.success(), "Godot 原生扩展构建失败");
|
||||
godot_bundle::validate(&root).unwrap_or_else(|error| panic!("{error}"));
|
||||
}
|
||||
|
||||
/// 构建期产物归位:只有构建过程才产出、因而无法由准备步骤生成的随包子目录。
|
||||
///
|
||||
/// 源码派生的子目录由准备步骤在 `tauri dev|build` 之前写入;这里只补构建期才存在的产物。
|
||||
/// 把这批产物也归位到准备步骤(连同编辑器分支产物)在后续里程碑完成。
|
||||
fn stage_build_generated_plugin_payloads(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
if !package_layout::plugin_staging_applies(&target) {
|
||||
return;
|
||||
}
|
||||
let declared = package_layout::plugins();
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
let plugins = package_layout::plugin_directories(
|
||||
&repo_root.join(declared.source_directory),
|
||||
declared.manifest_file_name,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
for plugin in plugins {
|
||||
for subdirectory in declared.subdirectories {
|
||||
if !package_layout::subdirectory_is_build_derived(subdirectory)
|
||||
|| !package_layout::subdirectory_enabled(
|
||||
subdirectory,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
let source = plugin.path.join(&relative);
|
||||
if !source.is_dir() {
|
||||
continue;
|
||||
}
|
||||
copy_staged_tree(
|
||||
&source,
|
||||
&destination_root.join(&plugin.name).join(&relative),
|
||||
);
|
||||
}
|
||||
for staging in declared.library_staging {
|
||||
if plugin.name != staging.plugin
|
||||
|| !package_layout::library_staging_enabled(
|
||||
staging,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
|
||||
match staging.layout {
|
||||
"godot-bundle" => godot_bundle::stage(
|
||||
&plugin.path.join(&relative),
|
||||
&destination_root.join(&plugin.name).join(&relative),
|
||||
&target,
|
||||
true,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}")),
|
||||
other => panic!("未实现的随包库 staging 布局:{other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 复制一棵目录树(按声明跳过构建产物与测试文件);内容一致时不重写。
|
||||
fn copy_staged_tree(source: &std::path::Path, destination: &std::path::Path) {
|
||||
if !source.is_dir() {
|
||||
return;
|
||||
}
|
||||
fs::create_dir_all(destination).expect("创建插件资源目录失败");
|
||||
for entry in fs::read_dir(source).expect("读取插件资源失败").flatten() {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy().to_string();
|
||||
let target = destination.join(&file_name);
|
||||
assert!(
|
||||
entry
|
||||
.file_type()
|
||||
.expect("读取插件文件类型失败")
|
||||
.is_symlink(),
|
||||
"插件资源不允许符号链接"
|
||||
);
|
||||
if path.is_dir() {
|
||||
if !package_layout::skip_directory(&name) {
|
||||
copy_staged_tree(&path, &target);
|
||||
}
|
||||
} else if !package_layout::skip_file_name(&name) {
|
||||
let bytes = fs::read(&path).expect("读取插件资源失败");
|
||||
if fs::read(&target).is_ok_and(|existing| existing == bytes) {
|
||||
continue;
|
||||
}
|
||||
fs::write(&target, bytes).expect("复制插件资源失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
pub const BUNDLE_FILES: [&str; 4] = [
|
||||
"bin/win-x64/agc_godot_editor.dll",
|
||||
"bin/win-x64/metadata.json",
|
||||
"vendor/LICENSE.txt",
|
||||
"vendor/provenance.json",
|
||||
];
|
||||
// 共享随包资源声明:Godot 随包文件清单与构建期校验共用同一份来源。
|
||||
#[allow(dead_code)]
|
||||
#[path = "package_layout.rs"]
|
||||
mod package_layout;
|
||||
|
||||
pub const BUNDLE_FILES: &[&str] = package_layout::GODOT_BUNDLE_FILES;
|
||||
|
||||
fn plain_metadata(path: &Path) -> Result<fs::Metadata, String> {
|
||||
let metadata = fs::symlink_metadata(path)
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ pub const CODEX_VERSION: &str = "0.155.1";
|
||||
pub const CODEX_CLI_VERSION: &str = "codex-cli 0.155.1";
|
||||
pub const CODEX_MANIFEST_SCHEMA: &str = "genarrative-codex-sidecar.v2";
|
||||
|
||||
pub const GODOT_BUNDLE_FILES: &[&str] = &["bin/win-x64/agc_godot_editor.dll", "bin/win-x64/metadata.json", "vendor/LICENSE.txt", "vendor/provenance.json"];
|
||||
|
||||
pub const CODEX: Codex = Codex {
|
||||
package_metadata: PackageMetadata {
|
||||
layout_version: 1,
|
||||
@@ -75,6 +77,7 @@ pub const PLUGINS: Plugins = Plugins {
|
||||
subdirectories: &[
|
||||
Subdirectory {
|
||||
path: "src",
|
||||
plugin: "",
|
||||
origin: "source",
|
||||
target_contains: &[],
|
||||
targets: &[],
|
||||
@@ -82,6 +85,7 @@ Subdirectory {
|
||||
},
|
||||
Subdirectory {
|
||||
path: "panels",
|
||||
plugin: "",
|
||||
origin: "source",
|
||||
target_contains: &[],
|
||||
targets: &[],
|
||||
@@ -89,6 +93,7 @@ Subdirectory {
|
||||
},
|
||||
Subdirectory {
|
||||
path: "skills",
|
||||
plugin: "",
|
||||
origin: "source",
|
||||
target_contains: &[],
|
||||
targets: &[],
|
||||
@@ -96,14 +101,16 @@ Subdirectory {
|
||||
},
|
||||
Subdirectory {
|
||||
path: "native/payload",
|
||||
origin: "source",
|
||||
plugin: "agc-cocos-editor",
|
||||
origin: "prepared",
|
||||
target_contains: &["windows"],
|
||||
targets: &[],
|
||||
features: &[],
|
||||
features: &["cocos-editor-injection"],
|
||||
},
|
||||
Subdirectory {
|
||||
path: "dotnet/publish/win-x64",
|
||||
origin: "build",
|
||||
plugin: "agc-unity-editor",
|
||||
origin: "prepared",
|
||||
target_contains: &[],
|
||||
targets: &["x86_64-pc-windows-msvc"],
|
||||
features: &["unity-editor-execute"],
|
||||
|
||||
@@ -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-<platform>/vendor/<target>",
|
||||
"node_modules/@openai/codex/node_modules/@openai/codex-<platform>/vendor/<target>"
|
||||
],
|
||||
"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,31 +97,162 @@
|
||||
"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",
|
||||
"features": [
|
||||
"cocos-editor-injection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "dotnet/publish/win-x64",
|
||||
"origin": "build",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["unity-editor-execute"]
|
||||
"origin": "prepared",
|
||||
"prepare": "unity-helper-publish",
|
||||
"targets": [
|
||||
"x86_64-pc-windows-msvc"
|
||||
],
|
||||
"features": [
|
||||
"unity-editor-execute"
|
||||
],
|
||||
"plugin": "agc-unity-editor"
|
||||
}
|
||||
],
|
||||
"libraryStaging": [
|
||||
{
|
||||
"plugin": "agc-godot-editor",
|
||||
"sourceSubdirectory": "native/gdextension",
|
||||
"targets": ["x86_64-pc-windows-msvc"],
|
||||
"features": ["godot-editor-execute"],
|
||||
"layout": "godot-bundle"
|
||||
"prepare": "godot-extension-build",
|
||||
"targets": [
|
||||
"x86_64-pc-windows-msvc"
|
||||
],
|
||||
"features": [
|
||||
"godot-editor-execute"
|
||||
],
|
||||
"layout": "godot-bundle",
|
||||
"files": [
|
||||
"bin/win-x64/agc_godot_editor.dll",
|
||||
"bin/win-x64/metadata.json",
|
||||
"vendor/LICENSE.txt",
|
||||
"vendor/provenance.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"skipDirectoryNames": ["target", "node_modules"],
|
||||
"skipDirectoryNamePrefixes": ["."],
|
||||
"skipFileNamePrefixes": ["."],
|
||||
"skipFileNameFragments": [".test."]
|
||||
"nativePayloads": [
|
||||
{
|
||||
"plugin": "agc-cocos-editor",
|
||||
"prepare": "cocos-bridge-build",
|
||||
"sourceFileName": "cocos_editor_bridge.dll",
|
||||
"destinationSubdirectory": "native/payload",
|
||||
"targets": [
|
||||
"x86_64-pc-windows-msvc"
|
||||
],
|
||||
"features": [
|
||||
"cocos-editor-injection"
|
||||
],
|
||||
"destinationFileName": "cocos-editor-bridge.dll"
|
||||
}
|
||||
],
|
||||
"prepareSteps": [
|
||||
{
|
||||
"name": "unity-helper-publish",
|
||||
"kind": "powershell",
|
||||
"workingDirectory": "plugins/agc-unity-editor/dotnet",
|
||||
"scriptFileName": "build.ps1",
|
||||
"fingerprint": {
|
||||
"roots": [
|
||||
"."
|
||||
],
|
||||
"excludeDirectoryNames": [
|
||||
"bin",
|
||||
"obj",
|
||||
"publish",
|
||||
"native-build"
|
||||
],
|
||||
"stampRelativePath": "publish/win-x64/.agc-source.sha256"
|
||||
},
|
||||
"requiredOutputs": [
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/NOTICE",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/THIRD-PARTY-NOTICES.txt",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/DotCraft-Apache-2.0.txt",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/Roslyn-MIT.txt",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/upstream.json",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/dotnet-LICENSE.TXT",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/dotnet-THIRD-PARTY-NOTICES.TXT",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/microsoft.codeanalysis.common-ThirdPartyNotices.rtf",
|
||||
"plugins/agc-unity-editor/dotnet/publish/win-x64/licenses/microsoft.codeanalysis.csharp-ThirdPartyNotices.rtf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "godot-extension-build",
|
||||
"kind": "powershell",
|
||||
"workingDirectory": "plugins/agc-godot-editor/native/gdextension",
|
||||
"scriptFileName": "build.ps1",
|
||||
"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"
|
||||
],
|
||||
"requiredOutputs": []
|
||||
}
|
||||
],
|
||||
"skipDirectoryNames": [
|
||||
"target",
|
||||
"node_modules"
|
||||
],
|
||||
"skipDirectoryNamePrefixes": [
|
||||
"."
|
||||
],
|
||||
"skipFileNamePrefixes": [
|
||||
"."
|
||||
],
|
||||
"skipFileNameFragments": [
|
||||
".test."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -286,7 +293,7 @@ pub fn validate_staged_plugins(
|
||||
let relative = declared_relative_path(subdirectory.path);
|
||||
let source = plugin.path.join(&relative);
|
||||
let staged_directory = staged.join(&relative);
|
||||
if subdirectory_is_build_derived(subdirectory) {
|
||||
if subdirectory_is_prepared(subdirectory) {
|
||||
if source.exists() && !staged_directory.is_dir() {
|
||||
return Err(format!(
|
||||
"随包构建期产物缺失:{}(插件 {})",
|
||||
@@ -330,9 +337,9 @@ pub fn subdirectory_is_source_derived(subdirectory: &Subdirectory) -> bool {
|
||||
subdirectory.origin == "source"
|
||||
}
|
||||
|
||||
/// 声明为「由构建期工具链产出」的子目录。
|
||||
pub fn subdirectory_is_build_derived(subdirectory: &Subdirectory) -> bool {
|
||||
subdirectory.origin == "build"
|
||||
/// 声明为「需要先由准备步骤运行构建命令产出」的子目录。
|
||||
pub fn subdirectory_is_prepared(subdirectory: &Subdirectory) -> bool {
|
||||
subdirectory.origin == "prepared"
|
||||
}
|
||||
|
||||
/// 仓库插件目录:含清单文件的普通目录,按名字排序。
|
||||
@@ -853,14 +860,14 @@ mod tests {
|
||||
.filter(|entry| subdirectory_is_source_derived(entry))
|
||||
.map(|entry| entry.path)
|
||||
.collect::<Vec<_>>();
|
||||
let build = PLUGINS
|
||||
let prepared = PLUGINS
|
||||
.subdirectories
|
||||
.iter()
|
||||
.filter(|entry| subdirectory_is_build_derived(entry))
|
||||
.filter(|entry| subdirectory_is_prepared(entry))
|
||||
.map(|entry| entry.path)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(source, ["src", "panels", "skills", "native/payload"]);
|
||||
assert_eq!(build, ["dotnet/publish/win-x64"]);
|
||||
assert_eq!(source, ["src", "panels", "skills"]);
|
||||
assert_eq!(prepared, ["native/payload", "dotnet/publish/win-x64"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
| 字段 | 值 |
|
||||
| ----------- | --------------------------------------------------------------- |
|
||||
| Version | 1.0 |
|
||||
| Status | proposed |
|
||||
| Date | 2026-09-26 |
|
||||
| Version | 1.0 |
|
||||
| Status | in-progress(2026-09-27 实现完成,待 Windows 真机验收) |
|
||||
| Date | 2026-09-26 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md` |
|
||||
|
||||
## 已实现(2026-09-27)
|
||||
|
||||
- 声明新增三类「准备步骤」产物:`subdirectories[origin=prepared]`(Unity publish 目录)、`libraryStaging[].prepare + files`(Godot gdextension)、`nativePayloads[]`(Cocos bridge dll);`plugins.prepareSteps` 描述每个准备步骤的程序、工作目录、指纹与必需产物。
|
||||
- 准备步骤(`scripts/prepare-bundled-resources.mjs`)按声明执行 `powershell.exe -File build.ps1` 与 `cargo build -p … --target …`,用内容指纹跳过未变化的步骤,校验必需产物齐全后才复制;命中指纹且产物齐全时零写入。
|
||||
- 构建脚本删除了三处产物生成与整棵树复制(`prepare_unity_editor_helper`、`prepare_godot_editor_extension`、`stage_cocos_editor_payload`、`stage_build_generated_plugin_payloads` 及其辅助函数,共减少约 220 行),只保留只读校验:源码派生内容逐文件比对、已准备产物存在性、Godot 随包库沿用既有深度校验(`godot_bundle::validate`)。Godot 随包文件清单改由声明提供(单一来源)。
|
||||
- `.taurignore` 的两份 staging 条目已删除(构建期不再写 `resources/plugins`,无需忽略)。
|
||||
- 准备步骤的 Windows 侧行为**尚未在真机验证**:本机 macOS 只能跑通编译、声明门禁与用例(命令执行器在用例中注入假实现)。
|
||||
|
||||
## 目标
|
||||
|
||||
编辑器分支(Unity/Godot/Cocos)的随包产物也由准备步骤生成,构建脚本不再调用外部工具链产出随包资源;此前为绕开自触发问题而加入的症状层补丁与说明全部删除,实现形态与主规范一致。
|
||||
@@ -43,17 +51,19 @@
|
||||
|
||||
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` → 两次快照必须逐项一致。
|
||||
3. **构建新鲜度**:源码不变时连续两次 `cargo build --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,第二次应为秒级 `Finished`,且不再出现 `Compiling genarrative-ai-game-creator-shell`。
|
||||
4. **打包一致性**:出一次 Windows 安装包,核对包内 `plugins/` 下三种编辑器分支产物的路径与 sha256 与迁移前一致;`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --no-run`(会触发构建脚本与只读校验)必须通过。
|
||||
4. **打包一致性**:出一次 Windows 安装包,核对包内 `plugins/` 下三种编辑器分支产物的路径与 sha256 与迁移前一致;`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --no-run`(会触发构建脚本的只读校验)必须通过。
|
||||
5. **客户端启动**:进入 Unity/Godot/Cocos 编辑器分支各一次,确认对应 helper/扩展被加载,没有「缺少组件」类提示。
|
||||
6. **边界(负例)**
|
||||
- 删掉 `plugins/agc-unity-editor/dotnet/publish/` 且让工具链不可用后打包:准备步骤必须给出明确失败原因(缺工具链/缺产物),而不是静默产出缺组件的包。
|
||||
- 删掉 `target/agc-resource-staging.json` 再跑准备步骤:预期重新生成,产物内容不变(丢缓存只多一次哈希)。
|
||||
- `AGC_SKIP_RESOURCE_STAGING=1 cargo build`:只做只读校验;手工改 `resources/**` 一个字节即应失败。
|
||||
- 删掉 `plugins/agc-unity-editor/dotnet/publish/win-x64/.agc-source.sha256` 后重跑准备步骤:预期重新执行 dotnet publish,而不是复用旧产物。
|
||||
- 手工改 `resources/**` 一个字节后 `cargo build`:只读校验必须失败(源码派生内容逐文件比对)。
|
||||
|
||||
记录:把每步命令、关键输出与结论贴回本里程碑或对应 PR;未通过项回到主规范 §9 记为未决问题。
|
||||
|
||||
|
||||
@@ -9598,3 +9598,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 影响面:`apps/ai-game-creator-shell/scripts/{prepare-bundled-resources.mjs,prepare-bundled-resources.test.mjs,start-tauri-dev.mjs,build-release.mjs,build-release.test.mjs}`、`apps/ai-game-creator-shell/tests/start-tauri-dev.test.ts`、`src-tauri/build.rs`、`build_support/{package_layout.rs,package-layout.json,package-layout.generated.rs}`(`codex_package_metadata.rs` 删除)、`src/agent/codex_cli.rs`、技术方案 §4.9、M1/M2 里程碑与运维文档。
|
||||
- 验证:`cargo build --no-default-features` 连续三次 0.69 / 0.22 / 0.22 秒全程 fresh;强制构建脚本重跑(`touch build.rs`)后 `resources/codex` 与 `resources/plugins` 的快照(相对路径/大小/mtime/sha256)逐项不变;`cargo test --no-default-features … package_layout` 36 passed;`node --test scripts/prepare-bundled-resources.test.mjs` 10 passed(含上游元数据漂移被拒);`node --test scripts/build-release.test.mjs` 39 passed(含 `stage → bundled → build` 顺序与 no-bundle 不 staging);`npx vitest run tests/start-tauri-dev.test.ts` 12 passed(含「准备步骤先于 CLI 启动」)。
|
||||
- 边界(未验证):Windows 真机未验证,且 Unity publish 目录、Godot gdextension、Cocos payload 仍是构建期写入,Windows 构建新鲜度要等 M3 归位;完整 `npm run agc` 在本机被 SpacetimeDB `Pre-publish check`(先后 401 InvalidSignature 与 502 Bad Gateway,属既有本机环境问题)阻断,未跑通整条 dev 启动链路。
|
||||
|
||||
## 2026-09-27 AGC 编辑器分支产物归位:构建脚本彻底退出写入
|
||||
|
||||
- 背景:M2 之后构建脚本仍生成 Unity publish 目录、Godot gdextension 与 Cocos bridge payload,这三处写入落在 `resources/plugins/**`(`bundle.resources` 映射目录),Windows 上仍会触发每次重编,`.taurignore` 的 staging 条目也还不能删。
|
||||
- 决策(声明扩展):`subdirectories` 新增 `origin: prepared`;`libraryStaging` 增加 `prepare` 与 `files`;新增 `nativePayloads` 与 `plugins.prepareSteps`(程序类型、工作目录、指纹、必需产物)。Godot 随包文件清单改由声明提供——`godot_bundle::BUNDLE_FILES` 从生成的编译期常量取值,不再各写一份。
|
||||
- 决策(准备步骤执行器):准备步骤按声明运行 `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File build.ps1`(Unity/Godot,Godot 额外移除 `PSModulePath`)与 `cargo build -p cocos-editor-bridge --target … --features windows-injection`;内容指纹命中且必需产物齐全时零写入;命令执行器可注入,便于在 macOS 上用假执行器覆盖调度、指纹与失败关闭逻辑。
|
||||
- 决策(构建脚本瘦身):删除 `prepare_unity_editor_helper`、`prepare_godot_editor_extension`、`stage_cocos_editor_payload`、`stage_build_generated_plugin_payloads` 及其辅助函数(build.rs 415 → 193 行),只留只读校验,并新增「已准备产物存在性 + Godot 随包库深度校验」;`AGC_SKIP_RESOURCE_STAGING` 开关随写入分支一并删除;两份只含 staging 条目的 `.taurignore` 删除。
|
||||
- 影响面:`apps/ai-game-creator-shell/src-tauri/build_support/{package-layout.json,package-layout.generated.rs,package_layout.rs,godot_bundle.rs}`、`src-tauri/build.rs`、`scripts/{prepare-bundled-resources.mjs,prepare-bundled-resources.test.mjs,check-package-layout.mjs,build-release.mjs}`、两份 `.taurignore`、技术方案 §4.9/§8、M3 里程碑、运维文档、决策日志与排障经验。
|
||||
- 验证:准备步骤 13 条用例通过(含三类准备步骤调度、指纹跳过、缺产物失败关闭、幂等与失败关闭);`npm run agc:bundled-resources:check` 通过;`cargo check --no-default-features` 通过(构建脚本仅剩只读校验,且不再出现在随包资源的写入路径上)。
|
||||
- 边界(未验证):Windows 真机未验证——powershell/cargo 两条命令路径、Unity/Godot/Cocos 产物归位、包内容一致性与客户端加载,需按 M3 里程碑的验收清单在 Windows 上确认。
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
> 策划历史条目边界:旧策划 V1/V2 已全部退役,当前入口仅使用 Design Agent。下文带日期的旧 Planning V2、Fast GDD、`plan.submit_gdd`、旧 IPC/模块记录仅用于追溯,不能作为恢复旧代码、身份门禁或专属测试的依据;共享问题需在现役调用上核查。现行合同见[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。
|
||||
|
||||
## 2026-09-27 随包资源的写入方按产物来源分界:源码派生归准备步骤,构建期产物仍归构建脚本
|
||||
## 2026-09-27 随包资源的写入方按产物来源分界:源码派生直接复制,需工具链的先由准备步骤产出
|
||||
|
||||
- **现象**:改造后看到 `resources/plugins` 下 `dotnet/publish/win-x64`、`native/gdextension`、`native/payload` 由构建过程补写,不是回归:这三处只有构建过程才产得出来(Unity helper 的 dotnet publish、Godot gdextension、Cocos cdylib),声明里对应 `origin: build` 或 `libraryStaging`。
|
||||
- **写法**:新增随包内容先判断来源——能从仓库源码复制就写进 `build_support/package-layout.json` 的 `subdirectories`(`origin: source`,准备步骤负责);需要外部工具链或同一次 cargo 构建产出的,留在构建脚本并在声明里标注,不要塞进准备步骤(它在 `tauri build` 之前运行,拿不到那些产物)。
|
||||
- **校验口径**:`origin: source` 的内容在构建期会与仓库源码逐文件比对(插件清单 + 逐文件 sha256 + 整树符号链接),手改 `resources/**` 会被 `cargo build` 直接拒绝;`origin: build` 只查存在性。`resources/plugins` 由准备步骤拥有,不要手工往里放文件。
|
||||
- **写法**:新增随包内容先判断来源——能从仓库源码复制就写进 `build_support/package-layout.json` 的 `subdirectories`(`origin: source`);需要外部工具链或同一次 cargo 构建才能产出的,写成 `origin: prepared` / `libraryStaging` / `nativePayloads`,并在 `plugins.prepareSteps` 里声明要跑的程序、工作目录、指纹与必需产物——**不要写进构建脚本**(构建脚本自 M3 起只做只读校验,不再生成任何随包资源)。
|
||||
- **校验口径**:`origin: source` 的内容在构建期会与仓库源码逐文件比对(插件清单 + 逐文件 sha256 + 整树符号链接),手改 `resources/**` 会被 `cargo build` 直接拒绝;`prepared` 只查存在性,Godot 随包库额外跑 `godot_bundle::validate` 的深度校验。
|
||||
- **准备步骤指纹**:声明了指纹的步骤(Unity)命中后不会重跑工具链,改 `plugins/**` 源码即失效;指纹戳文件(`publish/win-x64/.agc-source.sha256`)删掉只会多跑一次构建。`resources/plugins` 由准备步骤拥有,不要手工往里放文件。
|
||||
|
||||
## 2026-09-27 AGC 随包资源的布局只能改声明文件,生成物由门禁锁死
|
||||
|
||||
|
||||
@@ -156,19 +156,20 @@ 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 实况:构建期写入边界
|
||||
### 4.9 M2/M3 实况:构建期写入边界
|
||||
|
||||
入口接线后,「谁写随包资源」按**产物来源**分界(声明里的 `origin` 字段表达同一口径):
|
||||
「谁写随包资源」按产物来源分界,声明里的 `origin` 字段表达同一口径:
|
||||
|
||||
| 来源 | 例子 | 谁写 | 时机 |
|
||||
| --- | --- | --- | --- |
|
||||
| `source`:声明 + 仓库源码即可生成 | `resources/codex/**`、插件工作区的 `src`/`panels`/`skills`/`native/payload` | 准备步骤(Node) | `tauri dev` / `tauri build` 之前 |
|
||||
| `build`:只有构建过程才产出 | 插件工作区的 `dotnet/publish/win-x64`(Unity helper 发布物) | 构建脚本 | 产物生成之后、只读校验之前 |
|
||||
| 外部工具链产物 | `native/gdextension`(Godot)、Cocos payload | 构建脚本 | 同上(本里程碑不动,归位属 M3) |
|
||||
| `prepared` / `libraryStaging` / `nativePayloads`:需要外部工具链或同一次 cargo 构建 | Unity `dotnet/publish/win-x64`、Godot `native/gdextension`、Cocos `native/payload` | 准备步骤:先按声明运行 `powershell.exe -File build.ps1` 或 `cargo build -p … --target …`,再复制产物 | 同上 |
|
||||
|
||||
因此本里程碑后:Codex 与插件工作区的源码派生内容不再由构建脚本写入(macOS 上已实测连续三次 `cargo build --no-default-features` 为 0.69 / 0.22 / 0.22 秒全程 fresh);Windows 上仍有三处构建期写入落在 `resources/plugins/**`(Unity publish 目录、Godot gdextension、Cocos payload),Windows 的构建新鲜度要等 M3 把这三处也归位到准备步骤(准备步骤先跑各自的构建命令,再复制产物)才达标。
|
||||
M2 之后构建脚本只做只读校验;M3 之后它也不再生成任何随包资源(连编辑器分支产物一并交给准备步骤),并在校验阶段确认源码派生内容逐文件一致、已准备产物在位、Godot 随包库通过既有深度校验。因此源码不变时 `cargo` 稳定 fresh(macOS 实测连续三次 `cargo build --no-default-features` 为 0.69 / 0.22 / 0.22 秒),`resources/plugins` 也不再需要在 `.taurignore` 里忽略(两份 staging 条目已删除)。
|
||||
|
||||
准备步骤的 Windows 侧命令执行(powershell / cargo)只在本机无法验证,验收清单见 M3 里程碑规范。
|
||||
|
||||
## 5. 兼容与迁移
|
||||
|
||||
@@ -209,7 +210,7 @@ build script 只写 `OUT_DIR`/`target`;随包资源是它的输入。凡需要
|
||||
|---|---|---|
|
||||
| M1 校验器化 | 单一声明 + 生成门禁、`build.rs` 只读校验路径、准备步骤脚本(codex + plugins 两条纯复制路径)、缓存与原子替换 | 校验器在既有 staging 产物上全绿(`AGC_SKIP_RESOURCE_STAGING=1` 单独可跑),且不改变现有构建行为 |
|
||||
| M2 入口接线 | dev 与两个发布入口调用准备步骤;codex/plugins 的写入分支从 build.rs 移除;`cargo` 新鲜度与 dev 不再重建达标 | 已交付(2026-09-27):macOS 侧 §6 前三行达标(连续 `cargo build` 0.69 / 0.22 / 0.22 秒 fresh;`tauri dev` 全程 `Rebuilding application` 0 次、`Running DevCommand` 1 次);Windows 新鲜度待 M3 归位三处构建期产物后复验 |
|
||||
| M3 外部工具链归位与清理 | unity/godot/cocos 的产物生成移出 build.rs;删除 `.taurignore` 的 staging 条目与相关注释;文档收口 | 包内容逐项对得上,门禁全绿(Windows 真机验收) |
|
||||
| M3 外部工具链归位与清理 | unity/godot/cocos 的产物生成移出 build.rs;删除 `.taurignore` 的 staging 条目与相关注释;文档收口 | 已实现(2026-09-27):三处产物改由准备步骤按声明运行 powershell/cargo 后复制,build.rs 只剩只读校验,两份 `.taurignore` 已删除;**待 Windows 真机按验收清单确认** |
|
||||
|
||||
里程碑规范与单里程碑实现计划按 [`docs/【协作规范】规范驱动开发工作流-2026-09-12.md`](../【协作规范】规范驱动开发工作流-2026-09-12.md) 另立 `docs/project-memory/plans/` 下的临时文件;本方案是它们的主规范来源。
|
||||
|
||||
|
||||
@@ -86,9 +86,9 @@ Windows 本地 `npm run dev` / `npm run dev:api-server` / `npm run dev:bgfilter-
|
||||
|
||||
`npm run agc` 的 Tauri Cargo 走同一套本地 wrapper 规则:`apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 在启动 Tauri CLI 前调用 `scripts/dev.mjs` 导出的 `buildLocalRustProcessEnv`,把决定结果显式写进 `RUSTC_WRAPPER` 与 `CARGO_BUILD_RUSTC_WRAPPER`。用户级或仓库级 Cargo 配置里的 `rustc-wrapper`(本地常见为 `~/.cargo/config.toml` 的 sccache)只在环境变量非空时才会被覆盖,所以这两个变量必须由脚本写入而不能留空;否则本机 sccache daemon 状态损坏时,Tauri Cargo 的首次 rustc 探测(`failed to run rustc to learn about target-specific information`)就会中断整个 AGC 启动,而配套后端因为已经在用同一规则而能正常起来。AGC 启动日志出现 `[dev:rust]` 提示即为该规则生效。
|
||||
|
||||
AGC 随包资源(内置 Codex CLI、插件工作区)的布局与组件白名单只有一份人工声明:`apps/ai-game-creator-shell/src-tauri/build_support/package-layout.json`。Node 侧准备步骤直接读它,Rust 侧读由 `node scripts/check-package-layout.mjs --write`(仓库根 `npm run agc:bundled-resources:sync`)生成的 `build_support/package-layout.generated.rs`;门禁 `npm run agc:bundled-resources:check` 已进 `agc:typecheck` 链,两者不一致直接失败。改布局只能改声明文件再同步生成物,不要手改生成文件,也不要另写第二份白名单。准备步骤是 `node apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs`:写临时目录后原子替换、命中缓存不写任何文件、只替换本工具产物、失败即退出并给出可执行提示;其用例为 `npm run agc:bundled-resources:test`。内置 Codex CLI 的上游平台包来自仓库根 `npm ci`,缺失时工具会直接提示重新安装。构建脚本对既有随包产物做只读校验,`AGC_SKIP_RESOURCE_STAGING=1` 可跳过写入分支、只跑校验。
|
||||
AGC 随包资源(内置 Codex CLI、插件工作区)的布局与组件白名单只有一份人工声明:`apps/ai-game-creator-shell/src-tauri/build_support/package-layout.json`。Node 侧准备步骤直接读它,Rust 侧读由 `node scripts/check-package-layout.mjs --write`(仓库根 `npm run agc:bundled-resources:sync`)生成的 `build_support/package-layout.generated.rs`;门禁 `npm run agc:bundled-resources:check` 已进 `agc:typecheck` 链,两者不一致直接失败。改布局只能改声明文件再同步生成物,不要手改生成文件,也不要另写第二份白名单。准备步骤是 `node apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs`:写临时目录后原子替换、命中缓存不写任何文件、只替换本工具产物、失败即退出并给出可执行提示;其用例为 `npm run agc:bundled-resources:test`。内置 Codex CLI 的上游平台包来自仓库根 `npm ci`,缺失时工具会直接提示重新安装。构建脚本对既有随包产物做只读校验(不再有写入分支,因此也没有跳过写入的开关)。
|
||||
|
||||
随包资源由准备步骤在 Tauri 之前生成:`npm run agc` 在 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 里、spawn Tauri CLI 之前调用(日志以 `[ai-game-creator-shell]` 前缀给出命中缓存或重新生成);发布链在 `build-release.mjs` 的 `runTauriBuild` 内与 Node 运行时 staging 并列调用,`tauri build --no-bundle` 不强制 staging。构建脚本不再生成这些资源(只对既有产物做只读校验,`AGC_SKIP_RESOURCE_STAGING=1` 可只跑校验),因此源码不变时 `cargo build` 稳定 fresh。`resources/plugins` 由准备步骤拥有:不要手工往它下面放东西,准备步骤会按仓库 `plugins/` 与声明重建;**编辑器分支产物**(Unity `dotnet/publish/win-x64`、Godot `native/gdextension`、Cocos payload)目前仍由构建脚本在产物生成后写入,归位属后续里程碑。
|
||||
随包资源由准备步骤在 Tauri 之前生成:`npm run agc` 在 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 里、spawn Tauri CLI 之前调用(日志以 `[ai-game-creator-shell]` 前缀给出命中缓存或重新生成);发布链在 `build-release.mjs` 的 `runTauriBuild` 内与 Node 运行时 staging 并列调用,并传 `profile: 'release'`,`tauri build --no-bundle` 不强制 staging。**构建脚本完全不生成随包资源**(源码派生内容逐文件比对、已准备产物查存在性、Godot 随包库跑既有深度校验),源码不变时 `cargo build` 稳定 fresh。需要外部工具链或同一次 cargo 构建才能产出的内容(Unity publish 目录、Godot gdextension、Cocos bridge dll)也由准备步骤按 `build_support/package-layout.json` 的 `prepareSteps` 先运行 `powershell.exe -File build.ps1` 或 `cargo build -p … --target …` 再复制;这些步骤按内容指纹跳过未变化的情况。`resources/plugins` 由准备步骤拥有:不要手工往里放东西,准备步骤会按仓库 `plugins/` 与声明重建;`.taurignore` 的 staging 条目已删除(构建期不再写该目录)。
|
||||
|
||||
### 本地 Rust 构建缓存与磁盘上限
|
||||
|
||||
|
||||
Reference in New Issue
Block a user