WIP: AGC 随包资源 staging 归位:统一跨平台构建目标与增量缓存(#519) #520

Draft
suzmii wants to merge 32 commits from fix/agc-rust-staging-idempotency into master
39 changed files with 7541 additions and 903 deletions
+2
View File
@@ -58,6 +58,8 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
/apps/ai-game-creator-shell/src-tauri/resources/claude-agent/
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime-staging-*/
/apps/ai-game-creator-shell/src-tauri/resources/plugins-staging-*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/*-staging-*/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
/apps/mobile-shell/.expo/
-4
View File
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
+5 -1
View File
@@ -13,6 +13,10 @@
"skill-pack:check": "node scripts/check-skill-pack.mjs",
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
"bundled-resources:check": "node scripts/check-package-layout.mjs",
"bundled-resources:sync": "node scripts/check-package-layout.mjs --write",
"bundled-resources:prepare": "node scripts/prepare-bundled-resources.mjs",
"bundled-resources:test": "node --test scripts/prepare-bundled-resources.test.mjs",
"llm-status": "node scripts/run-cli-with-config.mjs --llm-status",
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
"config": "node scripts/game-creator-config-wizard.mjs",
@@ -24,7 +28,7 @@
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && npm run bundled-resources:check && node scripts/check-config.mjs"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "0.3.285",
@@ -11,6 +11,8 @@ import {
} from './agc-global-version.mjs';
import {
defaultEditorFeatures,
readCargoTarget,
resolveEditorFeatures,
withDefaultCargoFeatures,
} from './cargo-features.mjs';
import {
@@ -19,6 +21,10 @@ import {
resolveReleaseChannel,
} from './channel-identity.mjs';
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
import {
prepareBundledResources,
supportedHostTarget,
} from './prepare-bundled-resources.mjs';
import { stageNodeRuntime } from './stage-node-runtime.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
@@ -40,26 +46,7 @@ function defaultTarget() {
}
function explicitBuildTarget(args) {
let target;
const separator = args.indexOf('--');
const options = separator < 0 ? args : args.slice(0, separator);
for (let index = 0; index < options.length; index += 1) {
const argument = options[index];
let value;
if (argument === '--target' || argument === '-t') {
value = options[++index];
} else if (argument.startsWith('--target=')) {
value = argument.slice('--target='.length);
} else {
continue;
}
if (!value?.trim() || value.startsWith('-')) {
throw new Error('--target 缺少有效目标');
}
if (target !== undefined) throw new Error('不能重复指定 --target');
target = value.trim();
}
return target;
return readCargoTarget(args);
}
function validateReleaseTarget(target) {
@@ -469,10 +456,32 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
return configPath;
}
/// 随包资源必须在打包工具之前生成:构建脚本只做只读校验,不再生成。
export function stageBundledResources(
target,
{ prepare = prepareBundledResources, features } = {},
) {
const summaries = prepare({
target,
features: new Set(features ?? defaultEditorFeatures(target)),
profile: 'release',
log: (line) => console.log(`[ai-game-creator-shell] ${line}`),
});
for (const summary of summaries) {
console.log(`[ai-game-creator-shell] ${summary}`);
}
}
export function runTauriBuild(
args = [],
context = resolveReleaseContext(args),
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
{
spawn = spawnSync,
stageRuntime = stageNodeRuntime,
stageBundled = stageBundledResources,
platform = process.platform,
arch = process.arch,
} = {},
) {
if (
explicitBuildTarget(args) &&
@@ -480,14 +489,35 @@ export function runTauriBuild(
) {
throw new Error('构建参数与发布上下文目标不一致');
}
const tauriArguments = buildTauriBuildArguments(args, context.target);
const { channel, target } = context;
if (!args.includes('--no-bundle')) stageRuntime(target);
const configPath = writeChannelConfigFile(
channel,
target,
!args.includes('--no-bundle'),
const tauriArguments = buildTauriBuildArguments(
args,
context.target,
platform,
);
const bundling = !args.includes('--no-bundle');
// 无显式 target 的 no-bundle 由 Cargo 构建宿主平台,不使用默认发布目标。
const resourceTarget =
explicitBuildTarget(args) ||
(bundling ? context.target : supportedHostTarget(platform, arch));
const features = resolveEditorFeatures({
argv: tauriArguments,
target: resourceTarget ?? platform,
env: {},
});
const { channel } = context;
if (bundling) {
stageRuntime(resourceTarget);
}
if (resourceTarget) {
stageBundled(resourceTarget, { features });
} else {
console.log(
'[ai-game-creator-shell] 当前宿主平台不参与客户端随包资源构建,跳过资源准备',
);
}
// Linux smoke 不发布 updater,沿用渠道上下文;桌面宿主必须与实际构建目标一致。
const target = resourceTarget ?? context.target;
const configPath = writeChannelConfigFile(channel, target, bundling);
console.log(
`[ai-game-creator-shell] 渠道 ${channel} 端点配置:${configPath}`,
);
@@ -38,6 +38,7 @@ import {
AGC_PRODUCT_NAME,
resolveChannelInstallIdentity,
} from './channel-identity.mjs';
import { supportedHostTarget } from './prepare-bundled-resources.mjs';
const windowsTarget = 'x86_64-pc-windows-msvc';
const universalTarget = 'universal-apple-darwin';
@@ -504,6 +505,8 @@ test('packaged renderer receives the same channel as the updater manifest', () =
// 必须 stub:真实 staging 会用宿主平台(如 macOS 的 darwin/arm64)去对默认的
// Windows 目标做一致性校验,在非 Windows 主机上直接失败——本用例只关心渠道注入。
stageRuntime: () => {},
// 同上:随包资源准备会读取真实上游包,本用例只关心渠道环境变量。
stageBundled: () => {},
spawn: (_binary, _args, options) => {
spawnOptions = options;
return { status: 0 };
@@ -594,6 +597,8 @@ test('explicit macOS target drives version lookup, Tauri endpoint, artifact and
seenContexts.push(context);
runTauriBuild(args, context, {
stageRuntime: () => {},
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
stageBundled: () => {},
spawn: (_binary, command) => {
const configIndex = command.lastIndexOf('--config');
const config = JSON.parse(
@@ -843,6 +848,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
context,
{
stageRuntime: () => {},
stageBundled: () => {},
spawn: (_binary, command) => {
assert.ok(
command.includes(
@@ -960,6 +966,10 @@ test('release stages Node before Tauri and injects its resource mapping only for
assert.equal(target, windowsTarget);
events.push('stage');
},
stageBundled(target) {
assert.equal(target, windowsTarget);
events.push('bundled');
},
spawn(_binary, args) {
events.push('build');
const config = JSON.parse(
@@ -975,12 +985,24 @@ test('release stages Node before Tauri and injects its resource mapping only for
return { status: 0 };
},
});
assert.deepEqual(events, ['stage', 'build']);
assert.deepEqual(events, ['stage', 'bundled', 'build']);
events.length = 0;
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
stageRuntime() {
assert.fail('no-bundle must not stage resources');
assert.fail('no-bundle must not stage node runtime');
},
stageBundled(target, options) {
// app 构建本身就需要随包资源,因此 --no-bundle 也要 staging,
// 且必须拿到与 cargo 相同的 feature 集(避免「cargo 开、staging 没开」)。
assert.equal(target, windowsTarget);
assert.ok(
options?.features?.length > 0,
'bundled staging 必须收到 feature 集',
);
events.push('bundled');
},
spawn(_binary, args) {
events.push('build');
const config = JSON.parse(
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
);
@@ -993,12 +1015,16 @@ test('release stages Node before Tauri and injects its resource mapping only for
return { status: 0 };
},
});
assert.deepEqual(events, ['bundled', 'build']);
assert.throws(
() =>
runTauriBuild(['--target', windowsTarget], context, {
stageRuntime() {
throw new Error('missing runtime');
},
stageBundled() {
assert.fail('invalid runtime must prevent bundled staging');
},
spawn() {
assert.fail('invalid runtime must prevent build');
},
@@ -1007,6 +1033,297 @@ test('release stages Node before Tauri and injects its resource mapping only for
);
});
const windowsEditorFeatures = [
'cocos-editor-execute',
'unity-editor-execute',
'godot-editor-execute',
];
function uniqueSorted(values) {
return [...new Set(values ?? [])].sort();
}
function splitFeatureList(value) {
return String(value ?? '')
.split(',')
.map((name) => name.trim())
.filter(Boolean);
}
/// 最终命令行里 Cargo 实际会收到的 feature 集:tauri 自身的 `--features` / `-f`
/// 与 `--` 之后的 runner 参数(tauri 会把它们追加到 cargo 命令)都算数,
/// 随包资源准备必须拿到同一集合。
function cargoFeaturesFromCommand(command) {
const argv = command.slice(command.indexOf('--') + 1);
const features = [];
for (let index = 0; index < argv.length; index += 1) {
const argument = String(argv[index]);
if (argument.startsWith('--features=')) {
features.push(...splitFeatureList(argument.slice('--features='.length)));
} else if (argument === '--features' || argument === '-f') {
features.push(...splitFeatureList(argv[index + 1]));
index += 1;
} else if (/^-f.+/u.test(argument)) {
features.push(...splitFeatureList(argument.slice(2)));
}
}
return uniqueSorted(features);
}
/**
* PR520 的 CI 回归:`tauri build --no-bundle` 不带显式 target 时编译的是**宿主**平台,
* 而发布上下文默认目标始终是 Windows。随包资源与 feature 集必须跟着宿主走,
* 否则 Linux 宿主会拿着 Windows 目标去 staging(CI smoke 直接失败),
* 还会出现「cargo 没开 feature、staging 开了」的错配。
*/
test('no-bundle 在无随包资源声明的宿主(Linux)既不 staging 也不注入 Windows feature', () => {
const context = resolveReleaseContext(['--no-bundle'], {});
assert.equal(context.target, windowsTarget);
let command;
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
runTauriBuild(['--no-bundle'], context, {
platform: 'linux',
arch: 'x64',
stageRuntime() {
assert.fail('--no-bundle 不得准备 Node 运行时');
},
stageBundled(target) {
assert.fail(`Linux 宿主没有随包资源声明,不得 staging ${target}`);
},
spawn(_binary, args) {
command = args;
return { status: 0 };
},
});
});
assert.ok(command, '宿主不受声明覆盖时仍必须完成本次构建');
assert.ok(!command.includes('--target'), '宿主构建不得打显式 target');
assert.deepEqual(
cargoFeaturesFromCommand(command),
[],
'Linux 宿主不得注入 Windows 编辑器 feature',
);
});
test('no-bundle 在 macOS / Windows 宿主按真实宿主三元组准备随包资源', () => {
const context = resolveReleaseContext(['--no-bundle'], {});
for (const [platform, arch, hostTarget, expectedFeatures] of [
['darwin', 'arm64', 'aarch64-apple-darwin', []],
['darwin', 'x64', 'x86_64-apple-darwin', []],
['win32', 'x64', windowsTarget, windowsEditorFeatures],
]) {
const label = `${platform}/${arch}`;
let staged;
let command;
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
runTauriBuild(['--no-bundle'], context, {
platform,
arch,
stageRuntime() {
assert.fail('--no-bundle 不得准备 Node 运行时');
},
stageBundled(target, options) {
staged = { target, features: options?.features };
},
spawn(_binary, args) {
command = args;
return { status: 0 };
},
});
});
assert.equal(
staged?.target,
hostTarget,
`${label} 必须按宿主三元组 staging`,
);
assert.deepEqual(
uniqueSorted(staged?.features),
uniqueSorted(expectedFeatures),
`${label} staging feature 与宿主不一致`,
);
assert.deepEqual(
cargoFeaturesFromCommand(command),
uniqueSorted(expectedFeatures),
`${label} Cargo feature 与 staging 不一致`,
);
assert.ok(
!command.includes('--target'),
`${label} 宿主构建不得打显式 target`,
);
}
});
test('no-bundle 的显式 target 优先于宿主:跨平台 / 跨架构仍按参数准备', () => {
for (const [platform, arch, target, expectedFeatures] of [
['linux', 'x64', windowsTarget, windowsEditorFeatures],
['darwin', 'arm64', 'x86_64-apple-darwin', []],
]) {
const args = ['--no-bundle', '--target', target];
const context = resolveReleaseContext(args, {});
const label = `${platform}/${arch} → ${target}`;
let staged;
let command;
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
runTauriBuild(args, context, {
platform,
arch,
stageRuntime() {
assert.fail('--no-bundle 不得准备 Node 运行时');
},
stageBundled(stageTarget, options) {
staged = { target: stageTarget, features: options?.features };
},
spawn(_binary, argv) {
command = argv;
return { status: 0 };
},
});
});
assert.equal(staged?.target, target, `${label} 必须按显式 target staging`);
assert.ok(
command.includes(target),
`${label} 必须把显式 target 传给 Tauri`,
);
assert.deepEqual(
uniqueSorted(staged?.features),
uniqueSorted(expectedFeatures),
`${label} staging feature 与预期不一致`,
);
assert.deepEqual(
cargoFeaturesFromCommand(command),
uniqueSorted(expectedFeatures),
`${label} Cargo feature 与 staging 不一致`,
);
}
});
test('打包构建(非 no-bundle)的 staging 目标跟随发布上下文而不是宿主', () => {
const crossArch = 'x86_64-apple-darwin';
const context = resolveReleaseContext([], { AGC_BUILD_TARGET: crossArch });
assert.equal(context.target, crossArch);
let runtimeTarget;
let staged;
let command;
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
runTauriBuild([], context, {
platform: 'darwin',
arch: 'arm64',
stageRuntime(target) {
runtimeTarget = target;
},
stageBundled(target, options) {
staged = { target, features: options?.features };
},
spawn(_binary, args) {
command = args;
return { status: 0 };
},
});
});
assert.equal(runtimeTarget, crossArch);
assert.equal(
staged?.target,
crossArch,
'打包构建必须按发布上下文目标 staging',
);
assert.deepEqual(uniqueSorted(staged?.features), []);
assert.ok(
command.includes(crossArch),
'打包构建必须把发布上下文目标传给 Tauri',
);
assert.deepEqual(cargoFeaturesFromCommand(command), []);
});
test('staging feature 与最终 Cargo feature 一致:-f / --features / -- 之后的 runner 参数', () => {
const context = resolveReleaseContext(['--no-bundle'], {});
const cases = [
{
args: ['--no-bundle', '--features', 'custom-editor-feature'],
expected: ['custom-editor-feature'],
},
{
args: ['--no-bundle', '--features=custom-a,custom-b'],
expected: ['custom-a', 'custom-b'],
},
{
args: ['--no-bundle', '-f', 'custom-editor-feature'],
expected: ['custom-editor-feature'],
},
{
args: ['--no-bundle', '-fcustom-editor-feature'],
expected: ['custom-editor-feature'],
},
// `--` 之后的参数是 runner 参数:tauri 会把它们追加到 cargo 命令行
// (`tauri build --help`:Command line arguments passed to the runner),
// 因此 Cargo 最终拿到的是「注入的默认值 ∪ runner 参数」,staging 必须一致。
{
args: ['--no-bundle', '--', '--features=app-runner'],
expected: [...windowsEditorFeatures, 'app-runner'],
},
// 环境变量泄漏(Jenkins Job 参数)不得让 staging 与 Cargo 分叉。
{
args: ['--no-bundle'],
env: { AGC_DEV_CARGO_FEATURES: 'cocos-editor-execute' },
},
];
for (const entry of cases) {
const { args, expected } = entry;
const label = args.join(' ');
let staged;
let command;
withEnv({ AGC_DEV_CARGO_FEATURES: undefined, ...entry.env }, () => {
runTauriBuild(args, context, {
platform: 'win32',
arch: 'x64',
stageRuntime() {
assert.fail(`${label} 不得准备 Node 运行时`);
},
stageBundled(target, options) {
staged = { target, features: options?.features };
},
spawn(_binary, argv) {
command = argv;
return { status: 0 };
},
});
});
assert.equal(staged?.target, windowsTarget, label);
const cargoFeatures = cargoFeaturesFromCommand(command);
assert.deepEqual(
uniqueSorted(staged?.features),
cargoFeatures,
`${label}:staging feature 必须等于 Cargo 收到的 feature`,
);
if (expected) {
assert.deepEqual(
cargoFeatures,
uniqueSorted(expected),
`${label}:Cargo feature 与预期不一致`,
);
}
}
});
test('未注入 platform / arch 时按真实 process 平台取宿主随包目标', () => {
const hostTarget = supportedHostTarget(process.platform, process.arch);
const context = resolveReleaseContext(['--no-bundle'], {});
const events = [];
withEnv({ AGC_DEV_CARGO_FEATURES: undefined }, () => {
runTauriBuild(['--no-bundle'], context, {
stageRuntime() {
assert.fail('--no-bundle 不得准备 Node 运行时');
},
stageBundled(target) {
events.push(target);
},
spawn() {
return { status: 0 };
},
});
});
assert.deepEqual(events, hostTarget ? [hostTarget] : []);
});
test('channel manifest carries version, platform keys and signature', () => {
withSignedArtifact('陶泥儿_0.1.48_x64-setup.exe', (artifact) => {
withEnv({ AGC_UPDATE_RELEASE_NOTES: '修复与改进' }, () => {
@@ -1111,6 +1428,8 @@ for (const channel of ['release', 'beta-2']) {
);
runTauriBuild([`--target=${target}`], context, {
stageRuntime: () => {},
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
stageBundled: () => {},
spawn: (_binary, command) => {
const config = JSON.parse(
readFileSync(
@@ -1,24 +1,134 @@
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
export function withDefaultCargoFeatures(argv, features) {
const separator = argv.indexOf('--');
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
if (
!features.length ||
cargoArgs.some(
(value) =>
value === '--features' ||
value === '-f' ||
value.startsWith('--features=') ||
/^-f.+/u.test(value),
)
) {
// 只看第一个 `--` 之前的 Tauri 选项:runner 参数区里的 `--features` 会与这里注入的
// 默认值在 Cargo 侧合并,不能当作「用户已经指定过了」而跳过注入。
if (!features.length || tauriOptions(argv).some(hasExplicitFeatures)) {
return argv;
}
return [`--features=${features.join(',')}`, ...argv];
}
/// 第一个 `--` 之前是 Tauri 自己的选项,之后是 runner(Cargo)参数。
function tauriOptions(argv) {
const separator = argv.indexOf('--');
return separator < 0 ? argv : argv.slice(0, separator);
}
/**
* 交给 Tauri 与 Cargo 的参数区间:第一个 `--` 之前是 Tauri 选项,之后到第二个 `--` 是
* runner(Cargo)参数;第二个 `--` 之后由 Tauri 转交应用,不属于构建参数。
* dev 入口(`buildTauriArguments` 会把启动器的单个 `--` 补成 runner + 应用两段)与发布
* 入口都用这一份区间解析,两边的目标与 feature 必须来自同一个口径。
*/
function cargoArguments(argv) {
const separator = argv.indexOf('--');
if (separator < 0) return argv;
const applicationStart = argv.indexOf('--', separator + 1);
return applicationStart < 0 ? argv : argv.slice(0, applicationStart);
}
/// 四种拼写(`--features x` / `--features=x` / `-f x` / `-fx`)都算显式指定。
function hasExplicitFeatures(value) {
return (
value === '--features' ||
value === '-f' ||
value.startsWith('--features=') ||
/^-f.+/u.test(value)
);
}
function parseFeatureNames(raw) {
return String(raw)
.split(',')
.map((name) => name.trim())
.filter(Boolean);
}
export function defaultEditorFeatures(target) {
return target === 'win32' || target.includes('windows')
? ['cocos-editor-execute', 'unity-editor-execute', 'godot-editor-execute']
: [];
}
/**
* 显式 Cargo 目标解析:`--target <triple>` / `--target=<triple>` / `-t <triple>`。
* 应用参数区(第二个 `--` 之后)不参与解析。未显式指定返回 undefined;取值缺失、
* 取到另一个选项或重复指定都失败关闭。
* dev 入口与发布入口共用这一份解析:cargo 的构建目标与随包资源 staging 目标
* 必须来自同一个显式来源,不能一方读参数、另一方回退宿主默认值。
*/
export function readCargoTarget(argv = []) {
const options = cargoArguments(argv);
let target;
for (let index = 0; index < options.length; index += 1) {
const argument = String(options[index]);
let value;
if (argument === '--target' || argument === '-t') {
value = options[index + 1];
index += 1;
} else if (argument.startsWith('--target=')) {
value = argument.slice('--target='.length);
} else if (argument.startsWith('-t') && argument.length > 2) {
value = argument.slice(2).replace(/^=/u, '');
} else {
continue;
}
if (!value?.trim() || String(value).startsWith('-')) {
throw new Error('--target 缺少有效目标');
}
if (target !== undefined) throw new Error('不能重复指定 --target');
target = String(value).trim();
}
return target;
}
/**
* 单一声明式解析:本次构建实际生效的编辑器 feature 集。
* 解析顺序:命令行 `--features`(Cargo 真正收到的参数,不能被环境覆盖)
* > 环境变量 `AGC_DEV_CARGO_FEATURES`(存在即以它为准,空串即关闭默认 feature)
* > 目标平台默认值。
* 传入的必须是「最终交给 Tauri 的参数」:dev 入口与发布入口的 cargo 参数与随包资源
* 准备步骤都消费这里的返回值,否则会出现「cargo 开了 feature、staging 没开(或反之)」。
*/
export function resolveEditorFeatures({
argv = [],
target,
env = process.env,
} = {}) {
const fromArgv = featuresFromArgv(argv);
if (fromArgv) {
return fromArgv;
}
// 变量存在本身即声明「本次 feature 集由环境决定」:空串是有意的关闭,不是未设置。
if (env.AGC_DEV_CARGO_FEATURES !== undefined) {
return parseFeatureNames(env.AGC_DEV_CARGO_FEATURES);
}
return defaultEditorFeatures(target);
}
/// 汇总 cargo 参数区里出现在任何位置的 `--features` / `-f`(Cargo 会把多个 flag 合并);
/// 没有任何 feature flag 时返回 null。显式但为空(如 `--features=`)返回空数组。
export function featuresFromArgv(argv = []) {
const options = cargoArguments(argv);
const features = [];
let found = false;
for (let index = 0; index < options.length; index += 1) {
const value = String(options[index]);
if (value.startsWith('--features=')) {
found = true;
features.push(...parseFeatureNames(value.slice('--features='.length)));
} else if (value === '--features' || value === '-f') {
found = true;
index += 1;
const next = options[index];
// 变参 flag:后面不是选项时才是它的取值。
if (next !== undefined && !String(next).startsWith('-')) {
features.push(...parseFeatureNames(next));
}
} else if (value.startsWith('-f') && value.length > 2) {
found = true;
features.push(...parseFeatureNames(value.slice(2).replace(/^=/u, '')));
}
}
return found ? features : null;
}
@@ -1358,7 +1358,6 @@ for (const channel of ['release', 'beta-2']) {
const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
'resources/claude-agent': 'claude-agent',
...Object.fromEntries(
[
'codex-patch-parser',
@@ -1387,6 +1386,7 @@ const expectedBundledWindowsResources = {
'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json',
'resources/plugins': 'plugins',
'resources/claude-agent': 'claude-agent',
};
assert.deepEqual(
tauriConfig.bundle?.resources,
@@ -1399,6 +1399,13 @@ for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
);
}
// 基线配置同时服务 Linux(只有 CI 会编译壳 crate):随包资源由准备步骤按目标生成,
// 基线声明它们会让没有 node_modules 的平台在构建期就因资源缺失失败。
if (String(key).startsWith('resources/')) {
throw new Error(
'AI game creator shell base Tauri config must not require platform-only bundled resources',
);
}
}
assert.deepEqual(
windowsTauriConfig.bundle?.resources,
@@ -1438,6 +1445,7 @@ assert.deepEqual(
]),
),
['resources/plugins', 'plugins'],
['resources/claude-agent', 'claude-agent'],
]),
'macOS must bundle the complete native Codex layout and plugin workspace',
);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,10 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs';
import {
defaultEditorFeatures,
readCargoTarget,
resolveEditorFeatures,
withDefaultCargoFeatures,
} from './cargo-features.mjs';
import {
@@ -10,6 +12,10 @@ import {
resolveAgcDevEndpoint,
withAgcDevEndpointEnv,
} from './dev-port.mjs';
import {
prepareBundledResources,
supportedHostTarget,
} from './prepare-bundled-resources.mjs';
import {
isAiGameCreatorServer,
preflightExistingVite,
@@ -51,22 +57,51 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
}
// 开发和发行构建使用同一平台编辑器 feature 集合。
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
function readDevCargoFeatures(env = process.env) {
const override = env.AGC_DEV_CARGO_FEATURES;
if (override !== undefined) {
return override
.split(',')
.map((value) => value.trim())
.filter(Boolean);
}
return defaultEditorFeatures(process.platform);
// 可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭;
// 命令行显式 `--features` / `-f` 优先于环境变量(cargo 实际收到的是命令行参数)。
// 入参是「最终交给 Tauri 的参数」:默认平台由其中的显式 `--target` 决定,未指定才是宿主平台。
function readDevCargoFeatures(argv = []) {
return resolveEditorFeatures({
argv,
target: readCargoTarget(argv) ?? process.platform,
});
}
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
function withDevCargoFeatures(argv, features = readDevCargoFeatures(argv)) {
return withDefaultCargoFeatures(argv, features);
}
/// 随包资源必须在 Tauri 之前生成:构建脚本只做只读校验,不再生成资源。
/// 命中缓存的重复调用不写任何文件,因此每次 dev 启动都会先跑一次。
/// `argv` 是「最终交给 Tauri 的参数」;staging 目标与本次 cargo 构建目标同源:
/// 显式 `--target` 优先,未指定时回落宿主目标;宿主平台不受声明覆盖(如 Linux)时保持跳过,
/// 不为其新增随包资源准备。`features` 省略时按同一份参数解析,不允许「cargo 一套、staging 另一套」。
function prepareBundledResourcesBeforeTauri(
features,
argv = [],
{
prepare = prepareBundledResources,
log = console.log,
hostTarget = supportedHostTarget(),
} = {},
) {
const target = readCargoTarget(argv) ?? hostTarget;
if (!target) {
log(
'[ai-game-creator-shell] 当前平台不受随包资源声明覆盖,跳过随包资源准备',
);
return;
}
const summaries = prepare({
target,
features: new Set(features ?? readDevCargoFeatures(argv)),
log: (line) => log(`[ai-game-creator-shell] ${line}`),
});
for (const summary of summaries) {
log(`[ai-game-creator-shell] ${summary}`);
}
}
function spawnTauriCli(argv, { env = process.env } = {}) {
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
cwd: appRoot,
@@ -75,6 +110,17 @@ function spawnTauriCli(argv, { env = process.env } = {}) {
});
}
/// Tauri dev 的 Cargo 直接继承启动器环境,用户级 / 仓库级 Cargo 配置里的
/// `rustc-wrapper`(本地常见为 sccache)会在这里生效。本地 sccache daemon 状态
/// 一旦损坏,`cargo` 的首次 rustc 探测就会失败并阻断整个 AGC 启动;因此这里复用
/// `npm run dev` 的本地 Rust 环境规则,由脚本而不是本机 Cargo 配置决定 wrapper。
function buildTauriDevProcessEnv(endpoint, env = process.env) {
return buildLocalRustProcessEnv({
...withAgcDevEndpointEnv(endpoint, env),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
});
}
async function runTauriDev(
argv = process.argv.slice(2),
{
@@ -84,6 +130,7 @@ async function runTauriDev(
spawnCli = spawnTauriCli,
waitForCli = waitForChildTermination,
terminateTree = terminateChildTree,
prepareResources = prepareBundledResourcesBeforeTauri,
} = {},
) {
const endpoint = await resolveDevEndpoint();
@@ -119,6 +166,12 @@ async function runTauriDev(
}
try {
// 目标与 feature 都从「最终交给 Tauri 的参数」解析:启动器自己的 `--` 之后是应用参数,
// 不能参与解析;注入默认 feature 也不改变这份解析结果。
const devTauriArguments = buildTauriArguments(argv, endpoint.url);
const devFeatures = readDevCargoFeatures(devTauriArguments);
prepareResources(devFeatures, devTauriArguments);
// Windows 下 Vite 会监听资源目录;必须在启动前完成目录替换,避免 rename 被占用。
const preparation = prepareFrontend(endpoint, {
signal: preparationAbort.signal,
onChild(frontend) {
@@ -131,14 +184,11 @@ async function runTauriDev(
]);
if (!prepared || shutdownSignal) return 1;
const tauriArguments = buildTauriArguments(
withDevCargoFeatures(argv),
withDevCargoFeatures(argv, devFeatures),
endpoint.url,
);
child = spawnCli(tauriArguments, {
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
},
env: buildTauriDevProcessEnv(endpoint),
});
const childResult = waitForCli(child);
const outcome = await Promise.race([
@@ -225,7 +275,9 @@ function isDirectModuleExecution() {
export {
buildTauriArguments,
buildTauriDevProcessEnv,
isDirectModuleExecution,
prepareBundledResourcesBeforeTauri,
runTauriDev,
spawnTauriCli,
withDevCargoFeatures,
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,18 @@
//! 构建与运行共用的平台布局;只允许分发锁定原生包里的明确组件。
//!
//! 布局、组件白名单与版本常量来自唯一声明 `build_support/package-layout.json`
//! (Rust 侧经 `build_support/package-layout.generated.rs` 取得编译期常量,
//! 由 `scripts/check-package-layout.mjs` 生成并在门禁中校验一致)。
//! 本模块只读声明,不写任何随包资源。
pub const VERSION: &str = "0.155.1";
pub const CLI_VERSION: &str = "codex-cli 0.155.1";
pub const SCHEMA: &str = "genarrative-codex-sidecar.v2";
// 共享声明模块:构建脚本、运行期与测试各自只用到其中一部分,未用到的入口不算缺陷。
#[allow(dead_code)]
#[path = "package_layout.rs"]
pub(crate) mod package_layout;
pub const VERSION: &str = package_layout::CODEX_VERSION;
pub const CLI_VERSION: &str = package_layout::CODEX_CLI_VERSION;
pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA;
#[derive(Clone, Copy, Debug)]
pub struct Layout {
@@ -12,46 +22,13 @@ pub struct Layout {
pub files: &'static [&'static str],
}
const WINDOWS_FILES: &[&str] = &[
"bin/codex.exe",
"bin/codex-code-mode-host.exe",
"codex-path/rg.exe",
"codex-resources/codex-command-runner.exe",
"codex-resources/codex-windows-sandbox-setup.exe",
"codex-package.json",
];
const MAC_FILES: &[&str] = &[
"bin/codex",
"bin/codex-code-mode-host",
"codex-path/rg",
"codex-resources/zsh/bin/zsh",
"codex-package.json",
];
pub fn for_target(target: &str) -> Option<Layout> {
match target {
"x86_64-pc-windows-msvc" => Some(Layout {
platform: "win32-x64",
directory: "win-x64",
executable: "bin/codex.exe",
files: WINDOWS_FILES,
}),
"aarch64-apple-darwin" | "x86_64-apple-darwin" => Some(Layout {
platform: if target.starts_with("aarch64") {
"darwin-arm64"
} else {
"darwin-x64"
},
directory: if target.starts_with("aarch64") {
"mac-native/darwin-arm64"
} else {
"mac-native/darwin-x64"
},
executable: "bin/codex",
files: MAC_FILES,
}),
_ => None,
}
package_layout::codex_target(target).map(|declared| Layout {
platform: declared.platform,
directory: declared.directory,
executable: declared.executable,
files: declared.files,
})
}
#[cfg(test)]
@@ -80,4 +57,11 @@ mod tests {
assert!(for_target("aarch64-pc-windows-msvc").is_none());
assert!(for_target("x86_64-unknown-linux-gnu").is_none());
}
#[test]
fn constants_come_from_the_shared_declaration() {
assert_eq!(VERSION, "0.155.1");
assert_eq!(CLI_VERSION, format!("codex-cli {VERSION}"));
assert_eq!(SCHEMA, "genarrative-codex-sidecar.v2");
}
}
@@ -1,57 +0,0 @@
//! 随包阶段的原生包元数据校验,不进入运行时生产模块。
use super::codex_bundle::{Layout, VERSION};
pub fn validate_package_metadata(
metadata: &serde_json::Value,
target: &str,
layout: Layout,
) -> Result<(), String> {
if metadata["layoutVersion"] == 1
&& metadata["version"] == VERSION
&& metadata["target"] == target
&& metadata["entrypoint"] == layout.executable
&& metadata["resourcesDir"] == "codex-resources"
&& metadata["pathDir"] == "codex-path"
{
Ok(())
} else {
Err(format!("Codex 原生包版本、布局或架构不匹配目标 {target}"))
}
}
#[cfg(test)]
mod tests {
use super::super::codex_bundle::for_target;
use super::*;
#[test]
fn metadata_rejects_version_architecture_and_layout_drift() {
let target = "aarch64-apple-darwin";
let layout = for_target(target).unwrap();
let valid = serde_json::json!({
"layoutVersion": 1,
"version": VERSION,
"target": target,
"entrypoint": "bin/codex",
"resourcesDir": "codex-resources",
"pathDir": "codex-path",
});
assert!(validate_package_metadata(&valid, target, layout).is_ok());
for (key, value) in [
("layoutVersion", serde_json::json!(2)),
("version", serde_json::json!("0.0.0")),
("target", serde_json::json!("x86_64-apple-darwin")),
("entrypoint", serde_json::json!("bin/codex.exe")),
("resourcesDir", serde_json::json!("../private")),
("pathDir", serde_json::json!(null)),
] {
let mut invalid = valid.clone();
invalid[key] = value;
assert!(
validate_package_metadata(&invalid, target, layout).is_err(),
"{key}"
);
}
}
}
@@ -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::*;
@@ -134,82 +101,20 @@ mod tests {
}
#[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();
fn validate_rejects_incomplete_bundle() {
let temp = tempfile::tempdir().expect("tempdir");
let error = validate(temp.path()).expect_err("空目录必须被拒绝");
assert!(!error.is_empty(), "失败原因不能为空");
// 只有文件名、内容不合法的「像样」目录也必须被拒绝。
for relative in BUNDLE_FILES {
assert_eq!(
fs::read(source.path().join(relative)).unwrap(),
fs::read(destination.path().join(relative)).unwrap()
);
let file = temp.path().join(relative);
fs::create_dir_all(file.parent().expect("parent")).expect("create dir");
fs::write(&file, b"not a real artifact").expect("write file");
}
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")));
assert!(
validate(temp.path()).is_err(),
"内容不合法的随包库必须被拒绝",
);
}
}
@@ -0,0 +1,163 @@
// @generated by apps/ai-game-creator-shell/scripts/check-package-layout.mjs
// 来源:build_support/package-layout.json。不要手工编辑本文件。
// 修改随包资源布局请编辑声明文件,然后运行
// npm run agc:bundled-resources:sync(在仓库根目录)
// 门禁会校验两者一致(npm run agc:typecheck 链内含 check-package-layout.mjs)。
pub const DECLARATION_SCHEMA: &str = "agc-package-layout.v1";
pub const LAYOUT_VERSION: u64 = 2;
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 CLAUDE_AGENT_SDK_VERSION: &str = "0.3.285";
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,
resources_dir: "codex-resources",
path_dir: "codex-path",
},
resource_directory: "resources/codex",
manifest_file_name: "manifest.json",
package_metadata_file_name: "codex-package.json",
notice_file_name: "NOTICE.md",
source_roots: &["app", "repo"],
source_relative_paths: &["node_modules/@openai/codex-<platform>/vendor/<target>", "node_modules/@openai/codex/node_modules/@openai/codex-<platform>/vendor/<target>"],
notice_sources: &[
NoticeSource {
targets: &["aarch64-apple-darwin", "x86_64-apple-darwin"],
source: "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
preserve: false,
},
NoticeSource {
targets: &["x86_64-pc-windows-msvc"],
source: "resources/codex/win-x64/NOTICE.md",
preserve: true,
}
],
universal_groups: &[
UniversalGroup {
name: "mac-native",
directory: "mac-native",
targets: &["aarch64-apple-darwin", "x86_64-apple-darwin"],
}
],
targets: &[
CodexTarget {
target: "x86_64-pc-windows-msvc",
platform: "win32-x64",
directory: "win-x64",
executable: "bin/codex.exe",
files: &["bin/codex.exe", "bin/codex-code-mode-host.exe", "codex-path/rg.exe", "codex-resources/codex-command-runner.exe", "codex-resources/codex-windows-sandbox-setup.exe", "codex-package.json"],
},
CodexTarget {
target: "aarch64-apple-darwin",
platform: "darwin-arm64",
directory: "mac-native/darwin-arm64",
executable: "bin/codex",
files: &["bin/codex", "bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh", "codex-package.json"],
},
CodexTarget {
target: "x86_64-apple-darwin",
platform: "darwin-x64",
directory: "mac-native/darwin-x64",
executable: "bin/codex",
files: &["bin/codex", "bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh", "codex-package.json"],
}
],
};
pub const CLAUDE_AGENT: ClaudeAgent = ClaudeAgent {
version: "0.3.285",
resource_directory: "resources/claude-agent",
entry_relative_path: "agent-sidecar/src/index.mjs",
entry_file_name: "index.mjs",
node_modules_directory: "node_modules",
sdk_package_name: "@anthropic-ai/claude-agent-sdk",
sdk_entry_file_name: "sdk.mjs",
sdk_package_metadata_file_name: "package.json",
targets: &[
ClaudeAgentTarget {
target: "x86_64-pc-windows-msvc",
runtime_package: "claude-agent-sdk-win32-x64",
runtime_file_name: "claude.exe",
},
ClaudeAgentTarget {
target: "aarch64-apple-darwin",
runtime_package: "claude-agent-sdk-darwin-arm64",
runtime_file_name: "claude",
},
ClaudeAgentTarget {
target: "x86_64-apple-darwin",
runtime_package: "claude-agent-sdk-darwin-x64",
runtime_file_name: "claude",
}
],
};
pub const PLUGINS: Plugins = Plugins {
source_directory: "plugins",
destination_directory: "resources/plugins",
manifest_file_name: "plugin.json",
target_contains_any: &["windows", "apple-darwin"],
subdirectories: &[
Subdirectory {
path: "src",
plugin: "",
origin: "source",
target_contains: &[],
targets: &[],
features: &[],
},
Subdirectory {
path: "panels",
plugin: "",
origin: "source",
target_contains: &[],
targets: &[],
features: &[],
},
Subdirectory {
path: "skills",
plugin: "",
origin: "source",
target_contains: &[],
targets: &[],
features: &[],
},
Subdirectory {
path: "native/payload",
plugin: "agc-cocos-editor",
origin: "prepared",
target_contains: &["windows"],
targets: &[],
features: &["cocos-editor-injection"],
},
Subdirectory {
path: "dotnet/publish/win-x64",
plugin: "agc-unity-editor",
origin: "prepared",
target_contains: &[],
targets: &["x86_64-pc-windows-msvc"],
features: &["unity-editor-execute"],
}
],
library_staging: &[
LibraryStaging {
plugin: "agc-godot-editor",
source_subdirectory: "native/gdextension",
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"],
}
],
skip_directory_names: &["target", "node_modules"],
skip_directory_name_prefixes: &["."],
skip_file_name_prefixes: &["."],
skip_file_name_fragments: &[".test."],
};
@@ -0,0 +1,234 @@
{
"schema": "agc-package-layout.v1",
"description": "AGC 随包资源布局与复制规则的唯一声明。Rust 侧构建期校验与 Node 侧准备步骤共用本文件,任何一侧都不得再写第二份布局或组件白名单。含 <platform>、<target> 占位符的字段由调用方按目标三元展开。修改布局时同步递增 layoutVersion(准备步骤的缓存 key 组成部分)。",
"layoutVersion": 2,
"codex": {
"version": "0.155.1",
"cliVersionPrefix": "codex-cli ",
"manifestSchema": "genarrative-codex-sidecar.v2",
"packageMetadata": {
"layoutVersion": 1,
"resourcesDir": "codex-resources",
"pathDir": "codex-path"
},
"resourceDirectory": "resources/codex",
"manifestFileName": "manifest.json",
"packageMetadataFileName": "codex-package.json",
"noticeFileName": "NOTICE.md",
"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"],
"source": "resources/codex/【声明】Mac内置Codex组件-2026-09-18.md",
"preserve": false
},
{
"targets": ["x86_64-pc-windows-msvc"],
"source": "resources/codex/win-x64/NOTICE.md",
"preserve": true
}
],
"universalGroups": [
{
"name": "mac-native",
"directory": "mac-native",
"targets": ["aarch64-apple-darwin", "x86_64-apple-darwin"]
}
],
"targets": [
{
"target": "x86_64-pc-windows-msvc",
"platform": "win32-x64",
"directory": "win-x64",
"executable": "bin/codex.exe",
"files": [
"bin/codex.exe",
"bin/codex-code-mode-host.exe",
"codex-path/rg.exe",
"codex-resources/codex-command-runner.exe",
"codex-resources/codex-windows-sandbox-setup.exe",
"codex-package.json"
]
},
{
"target": "aarch64-apple-darwin",
"platform": "darwin-arm64",
"directory": "mac-native/darwin-arm64",
"executable": "bin/codex",
"files": [
"bin/codex",
"bin/codex-code-mode-host",
"codex-path/rg",
"codex-resources/zsh/bin/zsh",
"codex-package.json"
]
},
{
"target": "x86_64-apple-darwin",
"platform": "darwin-x64",
"directory": "mac-native/darwin-x64",
"executable": "bin/codex",
"files": [
"bin/codex",
"bin/codex-code-mode-host",
"codex-path/rg",
"codex-resources/zsh/bin/zsh",
"codex-package.json"
]
}
]
},
"claudeAgent": {
"version": "0.3.285",
"resourceDirectory": "resources/claude-agent",
"entryRelativePath": "agent-sidecar/src/index.mjs",
"entryFileName": "index.mjs",
"nodeModulesDirectory": "node_modules",
"sdkPackageName": "@anthropic-ai/claude-agent-sdk",
"sdkEntryFileName": "sdk.mjs",
"sdkPackageMetadataFileName": "package.json",
"sourceRoots": ["app", "repo"],
"skipDirectoryNames": ["target", "node_modules"],
"skipDirectoryNamePrefixes": ["."],
"skipFileNamePrefixes": ["."],
"skipFileNameFragments": [".test."],
"targets": [
{
"target": "x86_64-pc-windows-msvc",
"runtimePackage": "claude-agent-sdk-win32-x64",
"runtimeFileName": "claude.exe"
},
{
"target": "aarch64-apple-darwin",
"runtimePackage": "claude-agent-sdk-darwin-arm64",
"runtimeFileName": "claude"
},
{
"target": "x86_64-apple-darwin",
"runtimePackage": "claude-agent-sdk-darwin-x64",
"runtimeFileName": "claude"
}
]
},
"plugins": {
"sourceDirectory": "plugins",
"destinationDirectory": "resources/plugins",
"manifestFileName": "plugin.json",
"targetContainsAny": ["windows", "apple-darwin"],
"subdirectories": [
{
"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": "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",
"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"
]
}
],
"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."]
}
}
File diff suppressed because it is too large Load Diff
@@ -304,7 +304,8 @@ pub(crate) fn game_creator_claude_code_cli_version_identity() -> Result<String,
return Err("Claude Agent SDK sidecar 的 Node runtime 版本检查失败".to_string());
}
Ok(format!(
"claude-agent-sdk-sidecar@0.3.285;node={};entry={}",
"claude-agent-sdk-sidecar@{};node={};entry={}",
env!("AGC_CLAUDE_AGENT_SDK_VERSION"),
String::from_utf8_lossy(&output.stdout).trim(),
sidecar.display()
))
@@ -8,11 +8,6 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
#[path = "../../build_support/codex_bundle.rs"]
pub(crate) mod codex_bundle;
// 复用构建端校验的既有单测,生产运行时只编译共享布局。
#[cfg(test)]
#[path = "../../build_support/codex_package_metadata.rs"]
mod codex_package_metadata;
const GAME_CREATOR_CODEX_CLI_EXECUTABLE: &str = "codex";
const GAME_CREATOR_CODEX_CLI_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
const GAME_CREATOR_CODEX_CLI_STDOUT_MAX_BYTES: usize = 4 * 1024 * 1024;
@@ -4,6 +4,11 @@
#[path = "../build_support/godot_bundle.rs"]
mod godot_bundle;
// 复用随包资源声明的既有单测(校验通过/拒绝用例),生产运行时只经 codex_bundle 使用布局。
#[cfg(test)]
#[path = "../build_support/package_layout.rs"]
mod package_layout;
use std::collections::BTreeMap;
use std::fs;
use std::fs::{File, OpenOptions};
@@ -39,19 +39,14 @@
"vendor/codex-utils-path-uri/LICENSE": "licenses/codex-utils-path-uri/LICENSE",
"vendor/codex-utils-path-uri/NOTICE": "licenses/codex-utils-path-uri/NOTICE",
"vendor/codex-utils-absolute-path/LICENSE": "licenses/codex-utils-absolute-path/LICENSE",
"vendor/codex-utils-absolute-path/NOTICE": "licenses/codex-utils-absolute-path/NOTICE",
"resources/claude-agent": "claude-agent"
"vendor/codex-utils-absolute-path/NOTICE": "licenses/codex-utils-absolute-path/NOTICE"
},
"linux": {
"deb": {
"depends": [
"bubblewrap"
]
"depends": ["bubblewrap"]
},
"rpm": {
"depends": [
"bubblewrap"
]
"depends": ["bubblewrap"]
}
},
"icon": [

Some files were not shown because too many files have changed in this diff Show More