Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 547fb0dbce | |||
| 06ffa7b6ac | |||
| e4634928c6 | |||
| faa510e999 | |||
| 0b19df467e | |||
| 96b44d8660 | |||
| 33308a93e8 | |||
| f6dad1950f | |||
| a4b103a1b0 | |||
| 05d455d4ef | |||
| d8b37e0da9 | |||
| 21c3bd7a2b | |||
| 756297d2df | |||
| e02811588c | |||
| e1780b21b0 | |||
| 6ed1fd26ed | |||
| 4912aa4df0 |
@@ -57,6 +57,8 @@ temp*build*/
|
||||
/apps/ai-game-creator-shell/logs/
|
||||
/apps/ai-game-creator-shell/src-tauri/resources/node-runtime/
|
||||
/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/
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
||||
resources/plugins/
|
||||
@@ -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": {
|
||||
"@cubone/react-file-manager": "^1.35.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
resolveReleaseChannel,
|
||||
} from './channel-identity.mjs';
|
||||
import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs';
|
||||
import { prepareBundledResources } from './prepare-bundled-resources.mjs';
|
||||
import { stageNodeRuntime } from './stage-node-runtime.mjs';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
@@ -431,10 +432,30 @@ function writeChannelConfigFile(channel, target, includeNodeRuntime = false) {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
/// 随包资源必须在打包工具之前生成:构建脚本只做只读校验,不再生成。
|
||||
export function stageBundledResources(
|
||||
target,
|
||||
{ prepare = prepareBundledResources } = {},
|
||||
) {
|
||||
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) {
|
||||
console.log(`[ai-game-creator-shell] ${summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function runTauriBuild(
|
||||
args = [],
|
||||
context = resolveReleaseContext(args),
|
||||
{ spawn = spawnSync, stageRuntime = stageNodeRuntime } = {},
|
||||
{
|
||||
spawn = spawnSync,
|
||||
stageRuntime = stageNodeRuntime,
|
||||
stageBundled = stageBundledResources,
|
||||
} = {},
|
||||
) {
|
||||
if (
|
||||
explicitBuildTarget(args) &&
|
||||
@@ -444,7 +465,10 @@ export function runTauriBuild(
|
||||
}
|
||||
const tauriArguments = buildTauriBuildArguments(args, context.target);
|
||||
const { channel, target } = context;
|
||||
if (!args.includes('--no-bundle')) stageRuntime(target);
|
||||
if (!args.includes('--no-bundle')) {
|
||||
stageRuntime(target);
|
||||
stageBundled(target);
|
||||
}
|
||||
const configPath = writeChannelConfigFile(
|
||||
channel,
|
||||
target,
|
||||
|
||||
@@ -363,6 +363,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 };
|
||||
@@ -456,6 +458,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(
|
||||
@@ -705,6 +709,7 @@ test('Windows remains the default and explicit Windows overrides macOS environme
|
||||
context,
|
||||
{
|
||||
stageRuntime: () => {},
|
||||
stageBundled: () => {},
|
||||
spawn: (_binary, command) => {
|
||||
assert.ok(
|
||||
command.includes(
|
||||
@@ -818,6 +823,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(
|
||||
@@ -829,11 +838,14 @@ 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']);
|
||||
runTauriBuild(['--no-bundle', '--target', windowsTarget], context, {
|
||||
stageRuntime() {
|
||||
assert.fail('no-bundle must not stage resources');
|
||||
},
|
||||
stageBundled() {
|
||||
assert.fail('no-bundle must not stage bundled resources');
|
||||
},
|
||||
spawn(_binary, args) {
|
||||
const config = JSON.parse(
|
||||
readFileSync(args[args.lastIndexOf('--config') + 1], 'utf8'),
|
||||
@@ -848,6 +860,9 @@ test('release stages Node before Tauri and injects its resource mapping only for
|
||||
stageRuntime() {
|
||||
throw new Error('missing runtime');
|
||||
},
|
||||
stageBundled() {
|
||||
assert.fail('invalid runtime must prevent bundled staging');
|
||||
},
|
||||
spawn() {
|
||||
assert.fail('invalid runtime must prevent build');
|
||||
},
|
||||
@@ -960,6 +975,8 @@ for (const channel of ['release', 'beta-2']) {
|
||||
);
|
||||
runTauriBuild([`--target=${target}`], context, {
|
||||
stageRuntime: () => {},
|
||||
// 必须 stub:随包资源准备会读取真实上游包与仓库插件工作区,本用例只关心参数。
|
||||
stageBundled: () => {},
|
||||
spawn: (_binary, command) => {
|
||||
const config = JSON.parse(
|
||||
readFileSync(
|
||||
|
||||
+636
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,6 +1,7 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { buildLocalRustProcessEnv } from '../../../scripts/dev.mjs';
|
||||
import {
|
||||
defaultEditorFeatures,
|
||||
withDefaultCargoFeatures,
|
||||
@@ -10,6 +11,10 @@ import {
|
||||
resolveAgcDevEndpoint,
|
||||
withAgcDevEndpointEnv,
|
||||
} from './dev-port.mjs';
|
||||
import {
|
||||
prepareBundledResources,
|
||||
supportedHostTarget,
|
||||
} from './prepare-bundled-resources.mjs';
|
||||
import {
|
||||
isAiGameCreatorServer,
|
||||
preflightExistingVite,
|
||||
@@ -67,6 +72,29 @@ function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
|
||||
return withDefaultCargoFeatures(argv, features);
|
||||
}
|
||||
|
||||
/// 随包资源必须在 Tauri 之前生成:构建脚本只做只读校验,不再生成资源。
|
||||
/// 命中缓存的重复调用不写任何文件,因此每次 dev 启动都会先跑一次。
|
||||
function prepareBundledResourcesBeforeTauri(
|
||||
features = readDevCargoFeatures(),
|
||||
{ prepare = prepareBundledResources, log = console.log } = {},
|
||||
) {
|
||||
const target = supportedHostTarget();
|
||||
if (!target) {
|
||||
log(
|
||||
'[ai-game-creator-shell] 当前平台不受随包资源声明覆盖,跳过随包资源准备',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const summaries = prepare({
|
||||
target,
|
||||
features: new Set(features),
|
||||
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 +103,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 +123,7 @@ async function runTauriDev(
|
||||
spawnCli = spawnTauriCli,
|
||||
waitForCli = waitForChildTermination,
|
||||
terminateTree = terminateChildTree,
|
||||
prepareResources = prepareBundledResourcesBeforeTauri,
|
||||
} = {},
|
||||
) {
|
||||
const endpoint = await resolveDevEndpoint();
|
||||
@@ -130,15 +170,14 @@ async function runTauriDev(
|
||||
shutdownRequested.then(() => false),
|
||||
]);
|
||||
if (!prepared || shutdownSignal) return 1;
|
||||
const devFeatures = readDevCargoFeatures();
|
||||
prepareResources(devFeatures);
|
||||
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 +264,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::*;
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// @generated by apps/ai-game-creator-shell/scripts/check-package-layout.mjs
|
||||
// 来源:build_support/package-layout.json。不要手工编辑本文件。
|
||||
// 修改随包资源布局请编辑声明文件,然后运行
|
||||
// npm run agc:package-layout:sync(在仓库根目录)
|
||||
// 门禁会校验两者一致(npm run agc:typecheck 链内含 check-package-layout.mjs)。
|
||||
|
||||
pub const DECLARATION_SCHEMA: &str = "agc-package-layout.v1";
|
||||
pub const LAYOUT_VERSION: u64 = 1;
|
||||
|
||||
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,
|
||||
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 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",
|
||||
}
|
||||
],
|
||||
skip_directory_names: &["target", "node_modules"],
|
||||
skip_directory_name_prefixes: &["."],
|
||||
skip_file_name_prefixes: &["."],
|
||||
skip_file_name_fragments: &[".test."],
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"schema": "agc-package-layout.v1",
|
||||
"layoutVersion": 1,
|
||||
"description": "AGC 随包资源布局与复制规则的唯一声明。Rust 侧构建期校验与 Node 侧准备步骤共用本文件,任何一侧都不得再写第二份布局或组件白名单。含 <platform>、<target> 占位符的字段由调用方按目标三元展开。修改布局时同步递增 layoutVersion(准备步骤的缓存 key 组成部分)。",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
@@ -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;
|
||||
|
||||
@@ -15,6 +15,8 @@ const PROJECT_COMMAND_MAX_ARGUMENT_BYTES: usize = 8 * 1024;
|
||||
const PROJECT_COMMAND_MIN_TIMEOUT_SECONDS: u64 = 1;
|
||||
const PROJECT_COMMAND_MAX_TIMEOUT_SECONDS: u64 = 300;
|
||||
const PROJECT_COMMAND_OUTPUT_MAX_BYTES: usize = 24 * 1024;
|
||||
#[cfg(target_os = "linux")]
|
||||
const PROJECT_COMMAND_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const PROJECT_COMMAND_FINGERPRINT_MAX_ENTRIES: usize = 20_000;
|
||||
const PROJECT_COMMAND_FINGERPRINT_MAX_FILES: usize = 10_000;
|
||||
const PROJECT_COMMAND_FINGERPRINT_MAX_BYTES: u64 = 512 * 1024 * 1024;
|
||||
@@ -222,14 +224,30 @@ impl ProjectCommandTree {
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||
let requested = self.request_owned_group_termination();
|
||||
let _ = child.start_kill();
|
||||
let waited = tokio::time::timeout(Duration::from_secs(5), child.wait()).await;
|
||||
requested?;
|
||||
let waited = tokio::time::timeout_at(deadline, child.wait()).await;
|
||||
waited
|
||||
.map_err(|_| "等待受控命令主进程退出超时")?
|
||||
.map_err(|_| "受控命令主进程退出未确认")?;
|
||||
Ok("已请求终止受控进程组并回收主进程,完整子树状态未证明".into())
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let Self::Group { pid, .. } = self;
|
||||
// leader 可能与取消同时退出;只要组已停止便无需补发信号。
|
||||
if let Err(error) = wait_linux_project_command_group_exit(*pid, deadline).await {
|
||||
return Err(match requested {
|
||||
Ok(_) => error,
|
||||
Err(request_error) => format!("{request_error};{error}"),
|
||||
});
|
||||
}
|
||||
return Ok("主进程已回收,受控进程组已无活成员;完整子树状态未证明".into());
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
requested?;
|
||||
Ok("已请求终止受控进程组并回收主进程,完整子树状态未证明".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,17 +262,39 @@ impl ProjectCommandTree {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let Self::Group { pid, .. } = self;
|
||||
// 容器 PID 1 可能不回收 bwrap 的孤儿僵尸;它们不再执行,也无法被信号终止。
|
||||
// 仅在确认没有存活成员时免除清理,存活成员仍须通过 leader 身份核对。
|
||||
if !linux_project_command_group_has_live_members(*pid)? {
|
||||
return Ok(());
|
||||
}
|
||||
// wait 已回收 leader,不能再用旧 PID 授权发信号。
|
||||
// namespace 后代可能仍在退出,容器 PID 1 也可能保留孤儿僵尸。
|
||||
return wait_linux_project_command_group_exit(
|
||||
*pid,
|
||||
tokio::time::Instant::now() + PROJECT_COMMAND_CLEANUP_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
self.request_owned_group_termination().map(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn wait_linux_project_command_group_exit(
|
||||
group: u32,
|
||||
deadline: tokio::time::Instant,
|
||||
) -> Result<(), String> {
|
||||
while linux_project_command_group_has_live_members(group)? {
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return Err(
|
||||
"受控进程组仍有存活成员,退出未确认;不得向身份未确认的进程组补发信号".into(),
|
||||
);
|
||||
}
|
||||
tokio::time::sleep_until(
|
||||
deadline.min(tokio::time::Instant::now() + Duration::from_millis(20)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_project_command_group_has_live_members(group: u32) -> Result<bool, String> {
|
||||
let inspect = || -> std::io::Result<bool> {
|
||||
@@ -1842,6 +1882,17 @@ where
|
||||
),
|
||||
));
|
||||
}
|
||||
// 目标放行后可能立即退出,必须趁 ready gate 仍持有 launcher 时记录归属。
|
||||
let tree = match ProjectCommandTree::attach(&child) {
|
||||
Ok(tree) => tree,
|
||||
Err(error) => {
|
||||
let termination = terminate_project_command_process_group(&mut child).await;
|
||||
return Err(ProjectCommandError::new(
|
||||
ProjectCommandErrorStage::Preflight,
|
||||
project_command_launch_error_with_termination(error, termination),
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Err(error) = durable_commit() {
|
||||
let termination = terminate_project_command_process_group(&mut child).await;
|
||||
return Err(ProjectCommandError::new(
|
||||
@@ -1860,12 +1911,7 @@ where
|
||||
// cancelled future must not erase the launch-unknown decision window.
|
||||
let exec = gate.wait_target_exec(Duration::from_secs(3));
|
||||
match exec {
|
||||
Ok(TargetExecState::Established) => {
|
||||
let tree = ProjectCommandTree::attach(&child).map_err(|error| {
|
||||
ProjectCommandError::new(ProjectCommandErrorStage::LaunchUnknown, error)
|
||||
})?;
|
||||
Ok(EstablishedProjectCommand { tree, child, gate })
|
||||
}
|
||||
Ok(TargetExecState::Established) => Ok(EstablishedProjectCommand { tree, child, gate }),
|
||||
Ok(TargetExecState::Failed { errno }) => {
|
||||
let termination = terminate_project_command_process_group_after_commit(&mut child);
|
||||
Err(ProjectCommandError::new(
|
||||
@@ -1977,7 +2023,11 @@ async fn terminate_project_command_process_group(
|
||||
.ok_or_else(|| "请求终止受控进程组失败:子进程缺少 pid".to_string())?;
|
||||
let group_result = request_unix_project_command_process_group_termination(process_id);
|
||||
let child_kill_error = child.start_kill().err();
|
||||
let wait_result = child.wait().await;
|
||||
let deadline = tokio::time::Instant::now() + PROJECT_COMMAND_CLEANUP_TIMEOUT;
|
||||
let wait_result = tokio::time::timeout_at(deadline, child.wait())
|
||||
.await
|
||||
.map_err(|_| "等待受控命令主进程退出超时".to_string())?
|
||||
.map_err(|error| error.to_string());
|
||||
if let Err(error) = &group_result {
|
||||
let fallback = match (&child_kill_error, &wait_result) {
|
||||
(_, Ok(_)) => "主进程已回收,但无法确认其余组内进程".to_string(),
|
||||
@@ -1989,6 +2039,7 @@ async fn terminate_project_command_process_group(
|
||||
return Err(format!("{error};{fallback}"));
|
||||
}
|
||||
wait_result.map_err(|error| format!("请求终止受控进程组后等待主进程失败:{error}"))?;
|
||||
wait_linux_project_command_group_exit(process_id, deadline).await?;
|
||||
Ok(format!(
|
||||
"{}并完成主进程回收",
|
||||
group_result.expect("group termination result checked")
|
||||
@@ -2220,7 +2271,7 @@ where
|
||||
let (exit_code, timed_out, termination_summary) = match wait {
|
||||
ProjectCommandWait::Exited(Ok(status)) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
let _terminal = wait_established_project_command_terminal(gate).await?;
|
||||
let terminal = wait_established_project_command_terminal(gate).await;
|
||||
if let Err(error) = tree.after_main_exit(&mut child).await {
|
||||
stdout_task.abort();
|
||||
stderr_task.abort();
|
||||
@@ -2229,6 +2280,12 @@ where
|
||||
format!("command.exec 主进程退出后进程树未确认回收,需要人工核对:{error}"),
|
||||
));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Err(error) = terminal {
|
||||
stdout_task.abort();
|
||||
stderr_task.abort();
|
||||
return Err(error);
|
||||
}
|
||||
(status.code(), false, None)
|
||||
}
|
||||
ProjectCommandWait::Exited(Err(error)) => {
|
||||
@@ -2541,45 +2598,83 @@ mod tests {
|
||||
return;
|
||||
}
|
||||
assert_eq!(unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1) }, 0);
|
||||
let mut command = tokio::process::Command::new("/bin/sh");
|
||||
command
|
||||
.args(["-c", "sleep 60 & echo $!; read release"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped());
|
||||
command.as_std_mut().process_group(0);
|
||||
let mut child = command.spawn().unwrap();
|
||||
let tree = ProjectCommandTree::attach(&child).unwrap();
|
||||
let mut output = tokio::io::BufReader::new(child.stdout.take().unwrap());
|
||||
let mut line = String::new();
|
||||
tokio::io::AsyncBufReadExt::read_line(&mut output, &mut line)
|
||||
.await
|
||||
.unwrap();
|
||||
let descendant: i32 = line.trim().parse().unwrap();
|
||||
drop(child.stdin.take());
|
||||
child.wait().await.unwrap();
|
||||
let live_result = tree.after_main_exit(&mut child).await;
|
||||
assert_eq!(unsafe { libc::kill(descendant, libc::SIGKILL) }, 0);
|
||||
let mut info = unsafe { std::mem::zeroed::<libc::siginfo_t>() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
libc::waitid(
|
||||
libc::P_PID,
|
||||
descendant as u32,
|
||||
&mut info,
|
||||
libc::WEXITED | libc::WNOWAIT,
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
let zombie_result = tree.after_main_exit(&mut child).await;
|
||||
assert_eq!(
|
||||
unsafe { libc::waitpid(descendant, std::ptr::null_mut(), 0) },
|
||||
descendant
|
||||
);
|
||||
let error = live_result.expect_err("存活成员缺少 leader 身份时必须拒绝清理");
|
||||
assert!(error.contains("leader 身份未确认"), "{error}");
|
||||
zombie_result.expect("已回收 leader 的进程组只剩僵尸时不应要求人工核对");
|
||||
tree.after_main_exit(&mut child).await.unwrap();
|
||||
struct DescendantGuard(i32);
|
||||
impl Drop for DescendantGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::kill(self.0, libc::SIGKILL);
|
||||
libc::waitpid(self.0, std::ptr::null_mut(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 正常退出与取消/超时共用的 terminate 都覆盖 leader 已回收的窗口。
|
||||
for terminate in [false, true] {
|
||||
let mut command = tokio::process::Command::new("/bin/sh");
|
||||
command
|
||||
.args(["-c", "sleep 60 & echo $!; read release"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
command.as_std_mut().process_group(0);
|
||||
let mut child = command.spawn().unwrap();
|
||||
let tree = ProjectCommandTree::attach(&child).unwrap();
|
||||
let mut output = tokio::io::BufReader::new(child.stdout.take().unwrap());
|
||||
let mut line = String::new();
|
||||
tokio::io::AsyncBufReadExt::read_line(&mut output, &mut line)
|
||||
.await
|
||||
.unwrap();
|
||||
let descendant: i32 = line.trim().parse().unwrap();
|
||||
let descendant_guard = DescendantGuard(descendant);
|
||||
drop(child.stdin.take());
|
||||
child.wait().await.unwrap();
|
||||
let ProjectCommandTree::Group { pid, .. } = &tree;
|
||||
let error = tree
|
||||
.request_owned_group_termination()
|
||||
.expect_err("存活成员缺少 leader 身份时必须拒绝发送信号");
|
||||
assert!(error.contains("leader 身份未确认"), "{error}");
|
||||
let error = wait_linux_project_command_group_exit(*pid, tokio::time::Instant::now())
|
||||
.await
|
||||
.expect_err("持续存活成员必须在预算用尽时失败");
|
||||
assert!(error.contains("仍有存活成员"), "{error}");
|
||||
{
|
||||
let cleanup = async {
|
||||
if terminate {
|
||||
tree.terminate(&mut child).await.map(|_| ())
|
||||
} else {
|
||||
tree.after_main_exit(&mut child).await
|
||||
}
|
||||
};
|
||||
tokio::pin!(cleanup);
|
||||
// 先 poll 生产清理,确认它确实等待,才让后代进入僵尸态。
|
||||
tokio::select! {
|
||||
biased;
|
||||
result = &mut cleanup => panic!("后代仍存活时提前结束清理:{result:?}"),
|
||||
_ = tokio::task::yield_now() => {}
|
||||
}
|
||||
assert_eq!(unsafe { libc::kill(descendant, libc::SIGKILL) }, 0);
|
||||
let mut info = unsafe { std::mem::zeroed::<libc::siginfo_t>() };
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
libc::waitid(
|
||||
libc::P_PID,
|
||||
descendant as u32,
|
||||
&mut info,
|
||||
libc::WEXITED | libc::WNOWAIT,
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
cleanup.await.expect("leader 消失后,组成员停止应完成清理");
|
||||
}
|
||||
tree.after_main_exit(&mut child)
|
||||
.await
|
||||
.expect("僵尸不应阻止完成");
|
||||
wait_linux_project_command_group_exit(*pid, tokio::time::Instant::now())
|
||||
.await
|
||||
.expect("无活成员时不消耗等待预算");
|
||||
drop(descendant_guard);
|
||||
tree.after_main_exit(&mut child).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3426,6 +3521,9 @@ raise SystemExit(code)'
|
||||
.expect("run timeout test");
|
||||
assert!(timed_out.timed_out);
|
||||
assert_eq!(timed_out.status, "failed");
|
||||
#[cfg(target_os = "linux")]
|
||||
assert!(timed_out.output.contains("主进程已回收"));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
assert!(timed_out.output.contains("请求终止受控进程组"));
|
||||
assert!(timed_out.output.contains("不等同完整 OS sandbox"));
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../scripts/start-dev-stack.mjs';
|
||||
import {
|
||||
buildTauriArguments,
|
||||
buildTauriDevProcessEnv,
|
||||
runTauriDev as runTauriDevImpl,
|
||||
withDevCargoFeatures,
|
||||
} from '../scripts/start-tauri-dev.mjs';
|
||||
@@ -27,7 +28,13 @@ const resolveTestEndpoint = async () => testEndpoint;
|
||||
const runTauriDev = (
|
||||
argv: string[],
|
||||
options: Parameters<typeof runTauriDevImpl>[1],
|
||||
) => runTauriDevImpl(argv, { prepareFrontend: async () => {}, ...options });
|
||||
) =>
|
||||
runTauriDevImpl(argv, {
|
||||
prepareFrontend: async () => {},
|
||||
// 随包资源准备会读取真实上游包与仓库插件工作区;需要断言的用例自行注入。
|
||||
prepareResources: () => {},
|
||||
...options,
|
||||
});
|
||||
|
||||
async function waitForFile(path: string, timeoutMs = 5000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
@@ -152,6 +159,10 @@ describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||||
prepareFrontend: async () => {
|
||||
order.push('frontend-ready');
|
||||
},
|
||||
prepareResources: (features) => {
|
||||
expect(Array.isArray(features)).toBe(true);
|
||||
order.push('resources');
|
||||
},
|
||||
spawnCli: () => {
|
||||
order.push('spawn');
|
||||
return child;
|
||||
@@ -171,6 +182,7 @@ describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||||
expect(order).toEqual([
|
||||
'preflight',
|
||||
'frontend-ready',
|
||||
'resources',
|
||||
'spawn',
|
||||
'exit',
|
||||
'cleanup',
|
||||
@@ -300,3 +312,54 @@ describe('AI 游戏创作 Tauri dev 生命周期', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI 游戏创作 Tauri dev 进程环境', () => {
|
||||
const posixTest = process.platform === 'win32' ? test.skip : test;
|
||||
|
||||
// 本机 `~/.cargo/config.toml` 或仓库级 Cargo 配置里的 sccache wrapper 只有在环境变量
|
||||
// 非空时才会被覆盖;这里必须显式写入要交给 Tauri Cargo 的 wrapper 决策结果。
|
||||
posixTest('本地 dev 不把 sccache 交给 Tauri Cargo', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const env = buildTauriDevProcessEnv(testEndpoint, {
|
||||
RUSTC_WRAPPER: 'sccache',
|
||||
CARGO_BUILD_RUSTC_WRAPPER: 'sccache',
|
||||
});
|
||||
|
||||
expect(env.RUSTC_WRAPPER).toBe('/usr/bin/env');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('/usr/bin/env');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
posixTest('未显式配置 wrapper 时清空两个变量', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const env = buildTauriDevProcessEnv(testEndpoint, {
|
||||
CARGO_TERM_COLOR: 'never',
|
||||
});
|
||||
|
||||
expect(env.RUSTC_WRAPPER).toBe('');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('');
|
||||
expect(env.CARGO_TERM_COLOR).toBe('never');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
posixTest('保留显式自定义 wrapper 且不改写调用方 env', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const input = { RUSTC_WRAPPER: '/opt/custom/rustc-wrapper' };
|
||||
const env = buildTauriDevProcessEnv(testEndpoint, input);
|
||||
|
||||
expect(env.RUSTC_WRAPPER).toBe('/opt/custom/rustc-wrapper');
|
||||
expect(env.CARGO_BUILD_RUSTC_WRAPPER).toBe('/opt/custom/rustc-wrapper');
|
||||
expect(env.GENARRATIVE_AGC_VITE_PORT).toBe(String(testEndpoint.port));
|
||||
expect(input).toEqual({ RUSTC_WRAPPER: '/opt/custom/rustc-wrapper' });
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
- [AI 游戏创作智能体 App 实施计划](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md):当前 DirectProject、受控语义工具、UI workflow、资源和运行时合同。
|
||||
- [AGC 后端框架整理与演进路线](./technical/【技术方案】AGC后端框架整理与演进路线-2026-09-18.md):共享 Runtime、本地执行宿主、云端控制面、领域/平台适配器及分阶段收口边界。
|
||||
- [AGC 随包资源 staging 归位](./technical/【技术方案】AGC随包资源staging归位-2026-09-26.md):随包资源改由准备步骤在 `tauri dev|build` 之前一次性生成、`build.rs` 退化为校验者;含缓存与原子性合同、入口接线、验收判据与里程碑拆分。
|
||||
- [AGC 异步操作可恢复闭环](./【技术方案】AGC异步操作可恢复闭环-2026-09-14.md):认证响应体、最近项目检查和首页自动创建的超时、逐项恢复与跨页防重合同。
|
||||
- [AGC 客户端稳定版生命周期大切换](./【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md):统一 operation、认证/Runner、项目入口、本地恢复和 dev-stack 身份边界。
|
||||
- [策划会话 Runtime V2 接入与旧链路退役方案](./technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md):历史方案,仅用于追溯 V2 的实现与退役过程,不作为当前实现依据。
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 【实施计划】AGC 随包资源改由校验器读入
|
||||
|
||||
| 字段 | 值 |
|
||||
| --------- | --------------------------------------------------------------- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】AGC随包资源改由校验器读入-2026-09-26.md` |
|
||||
| Status | ready(待里程碑规范评审通过后开工) |
|
||||
| Owner | suzmii / Agent |
|
||||
|
||||
## 修改边界
|
||||
|
||||
允许修改:
|
||||
|
||||
- `apps/ai-game-creator-shell/src-tauri/build.rs`:新增只读校验调用点;本里程碑内保持现有写入分支不变(不改变既有构建行为)。
|
||||
- `apps/ai-game-creator-shell/src-tauri/build_support/**`:把平台布局、组件白名单、摘要校验整理为可被构建脚本之外的独立工具复用的一处声明。
|
||||
- 新增随包资源准备工具及其测试(位置见「待确认决策」)。
|
||||
- 需要时扩展 `apps/ai-game-creator-shell/scripts/check-config.mjs` 的断言。
|
||||
- 文档:主规范未决问题收口、开发运维文档对应段落。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- 三份 tauri 配置的 `resources` 映射、包内路径与安装包形态。
|
||||
- 运行时资源解析与完整性校验(`codex_cli.rs`、`plugin_host.rs`、`editor_adapters.rs`、`environment_check.rs`)。
|
||||
- 发布脚本流程、版本号机制、签名与上传。
|
||||
- dev 启动器与发布入口的接线(下一里程碑)。
|
||||
- 编辑器分支产物(Unity/Godot/Cocos)的生成方式(最后一个里程碑)。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. **共用能力可复用**:确认 `build_support` 内的平台布局与组件白名单能被独立工具引用(现状先例:`src/agent/codex_cli.rs` 与 `main.rs` 已通过 `#[path]` 复用同一模块),把「布局 + 白名单 + 摘要校验」收敛为单一入口,避免准备工具另写一份清单。
|
||||
2. **准备工具骨架**:目标目录与清单写出、缓存 key(上游 lockfile 的 `resolved` + `integrity` + 布局版本 + 目标三元)、临时目录 + 原子替换、所有权与符号链接校验、并发串行化、单行汇总日志。先实现纯复制两条路径(随包组件、插件工作区),编辑器分支产物本轮仍由构建脚本生成。
|
||||
3. **幂等与失败关闭**:重复执行不改变内容与时间戳;上游缺失、摘要不匹配、目录被非本工具占用、目标平台不支持四类场景各自失败并给出可定位原因。
|
||||
4. **校验路径上线**:构建脚本在既有产物上执行只读校验(默认不影响现有写入行为),校验失败以明确原因中止。
|
||||
5. **测试与证据**:按里程碑「证据要求」补齐用例与运行记录。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. 声明唯一性与门禁:`npm run agc:bundled-resources:check`(已进 `agc:typecheck` 链),不一致时用 `npm run agc:bundled-resources:sync` 重新生成。
|
||||
2. 准备工具用例(含幂等与失败关闭):`npm run agc:bundled-resources:test`。
|
||||
3. 校验路径独立运行(跳过写入分支):`AGC_SKIP_RESOURCE_STAGING=1 cargo check --no-default-features --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`。
|
||||
4. Rust 用例:`cargo test --no-default-features --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml package_layout` 与 `cargo test --no-default-features --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml codex_bundle`。
|
||||
5. 幂等(真实工作区):连续两次 `node apps/ai-game-creator-shell/scripts/prepare-bundled-resources.mjs`,第二次必须全部「命中缓存」,且两次之后的目录快照(相对路径、大小、mtime、sha256)完全一致。
|
||||
6. 并存一致:准备步骤产物与构建脚本产物逐文件比对(相对路径、大小、sha256)一致。
|
||||
7. 行为不回归:`cargo build --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --no-default-features`(本里程碑不承诺构建变快,仅确认行为与改造前一致,并记录当前构建耗时作为后续里程碑基线)。
|
||||
8. 门禁:`node apps/ai-game-creator-shell/scripts/check-config.mjs`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`,以及改动范围内相关 vitest/Rust 测试。
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
| 风险 | 影响 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| 校验器误判把构建卡死 | 影响所有本机构建 | 只读校验先以「不影响写入行为」的方式接入;出现误判可先关闭校验调用点回滚 |
|
||||
| 准备工具与构建脚本并存产生双写 | 两处结果漂移、时间戳变化 | 并存期以「准备工具产物 == 构建脚本产物」逐文件比对作为过渡判据;不一致视为失败 |
|
||||
| 缓存 key 漏掉上游变化 | 静默用旧组件 | key 含 lockfile `resolved` + `integrity` + 布局版本 + 三元;清单校验作为第二道闸 |
|
||||
| 准备工具实现形态选错 | 返工 | 见「待确认决策」,评审时一次定清 |
|
||||
|
||||
回滚点:本里程碑不改变既有构建行为,回滚只需移除校验调用点与准备工具,不影响产物与发布流程。
|
||||
|
||||
## 已定决策
|
||||
|
||||
准备工具的实现形态(主规范未决问题 1)**已定为混合**(2026-09-27,机制见主规范 §4.8):
|
||||
|
||||
- 上游获取、`integrity` 校验、归档安全与原子替换复用 Node 侧既有范式(`scripts/stage-node-runtime.mjs`、`scripts/prepare-macos-codex.mjs`);
|
||||
- 平台布局、组件白名单与逐文件摘要校验复用 Rust 侧既有声明(`build_support/codex_bundle.rs`、`build_support/godot_bundle.rs`),由准备工具与校验路径共用同一份声明文件承载,不再各写一份清单。(上游原生包元数据的期望值后来并入同一份声明;`build_support/codex_package_metadata.rs` 已在 M2 因失去调用方删除。)
|
||||
|
||||
理由:避免出现第二份组件白名单,同时不必重写 registry 下载、`integrity` 与 tar 安全校验;缺点是声明需要经过一次生成步骤才能在 Rust 侧使用,由 `check-package-layout.mjs` 门禁保证两者一致。
|
||||
@@ -0,0 +1,74 @@
|
||||
# 【里程碑】AGC 编辑器分支产物归位与症状层补丁清理
|
||||
|
||||
| 字段 | 值 |
|
||||
| ----------- | --------------------------------------------------------------- |
|
||||
| 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)的随包产物也由准备步骤生成,构建脚本不再调用外部工具链产出随包资源;此前为绕开自触发问题而加入的症状层补丁与说明全部删除,实现形态与主规范一致。
|
||||
|
||||
## 范围
|
||||
|
||||
- 三个编辑器分支产物的生成职责迁出构建脚本,包括需要外部工具链的两条路径。
|
||||
- 构建脚本内与资源生成相关的规避手段删除:残留清理逻辑、为幂等而设的辅助常量与判断、开发监听忽略条目中与随包资源相关的部分。
|
||||
- 平台与特性开关(哪些平台、哪些特性才需要这些产物)在新形态下保持既有语义。
|
||||
- 与发布打包、包内资源门禁、运行时解析的一致性核对。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 编辑器分支本身的接入协议、宿主能力与运行时行为。
|
||||
- 外部工具链版本管理与安装流程(沿用现状)。
|
||||
- 随包组件与插件工作区的生成形态(上一里程碑已完成)。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 前两个里程碑验收通过。
|
||||
- Windows 环境具备 Unity/Godot 分支所需的工具链(.NET 与 CMake 等),以便验证产物生成与打包。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 构建脚本中不再存在向随包资源目录写入的分支,也不再调用产出随包资源的外部工具链。
|
||||
- [ ] 三个编辑器分支的随包产物路径、内容摘要、可执行位与迁移前逐项一致,且由准备步骤稳定产出。
|
||||
- [ ] 此前为规避自触发而加入的补丁(残留清理、幂等辅助、监听忽略条目)在代码与文档中全部移除,不再有「为了绕开构建问题」的说明。
|
||||
- [ ] 平台与特性开关语义不变:不支持的平台不产出这些资源,且不因此失败。
|
||||
- [ ] 全量门禁通过,且客户端在具备条件与不具备条件两种环境下都能给出明确结论(可用 / 缺组件及原因)。
|
||||
|
||||
## 验证步骤(Windows 侧执行清单)
|
||||
|
||||
准备:切到本里程碑分支,`npm ci`(需装上 `@openai/codex-win32-x64`),确认 `spacetime --version` 与 `server-rs` 锁定版本一致(仅本地 dev 需要)。
|
||||
|
||||
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/` 下的扩展在位。
|
||||
- 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`(会触发构建脚本的只读校验)必须通过。
|
||||
5. **客户端启动**:进入 Unity/Godot/Cocos 编辑器分支各一次,确认对应 helper/扩展被加载,没有「缺少组件」类提示。
|
||||
6. **边界(负例)**
|
||||
- 删掉 `plugins/agc-unity-editor/dotnet/publish/` 且让工具链不可用后打包:准备步骤必须给出明确失败原因(缺工具链/缺产物),而不是静默产出缺组件的包。
|
||||
- 删掉 `target/agc-resource-staging.json` 再跑准备步骤:预期重新生成,产物内容不变(丢缓存只多一次哈希)。
|
||||
- 删掉 `plugins/agc-unity-editor/dotnet/publish/win-x64/.agc-source.sha256` 后重跑准备步骤:预期重新执行 dotnet publish,而不是复用旧产物。
|
||||
- 手工改 `resources/**` 一个字节后 `cargo build`:只读校验必须失败(源码派生内容逐文件比对)。
|
||||
|
||||
记录:把每步命令、关键输出与结论贴回本里程碑或对应 PR;未通过项回到主规范 §9 记为未决问题。
|
||||
|
||||
## 证据要求
|
||||
|
||||
- 自动化:编辑器分支产物的摘要对比、平台门槛用例、配置门禁与包内资源门禁。
|
||||
- 运行时:Windows 上一次完整打包与一次客户端启动,确认编辑器分支产物被读取。
|
||||
- 边界:缺少外部工具链、缺少组件、非目标平台三种情形下的失败与跳过语义。
|
||||
@@ -0,0 +1,51 @@
|
||||
# 【里程碑】AGC 随包资源改由校验器读入
|
||||
|
||||
| 字段 | 值 |
|
||||
| ----------- | ----------------------------------------------------------- |
|
||||
| Version | 1.0 |
|
||||
| Status | completed(2026-09-27) |
|
||||
| Date | 2026-09-26 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AGC随包资源staging归位-2026-09-26.md` |
|
||||
|
||||
## 已定决策(2026-09-27)
|
||||
|
||||
- **实现形态:混合**——生成归 Node(`scripts/prepare-bundled-resources.mjs`),声明与校验归 Rust。依据与对比见主规范 §4.8。
|
||||
- **单一声明**:`build_support/package-layout.json` 是唯一人工声明;Rust 侧使用由 `scripts/check-package-layout.mjs` 生成的编译期常量(`package-layout.generated.rs`),门禁 `npm run agc:bundled-resources:check` 已进 `agc:typecheck` 链;运行期 `codex_bundle.rs` 的公开接口与取值不变。
|
||||
- **校验收口边界**:本里程碑对随包 Codex 目录做全量校验(清单 schema/平台/版本、文件集合、逐文件摘要、第三方声明、可执行位、白名单外文件);插件随包目录只校验必需组件与符号链接。插件产物的逐文件摘要校验在 M2 由准备步骤写入树内清单后启用——M1 期间构建脚本仍整体重建 `resources/plugins`,树内清单会被清掉。
|
||||
- **独立验证入口**:`AGC_SKIP_RESOURCE_STAGING=1` 让构建脚本只跑只读校验、跳过写入分支。
|
||||
|
||||
## 目标
|
||||
|
||||
构建脚本不再需要「自己写随包资源」才能成立:在约定目录已有合规资源时,构建只做只读校验并通过;校验失败时给出明确原因并拒绝继续,而不是静默重新生成。
|
||||
|
||||
## 范围
|
||||
|
||||
- 随包资源的合规性判定:平台目录存在、清单 schema 与平台一致、逐文件摘要一致、必需组件齐全、版本与上游锁定一致。
|
||||
- 资源生成能力的可复用化:同一份布局与摘要校验能力既能被构建期校验使用,也能被准备步骤使用,不得出现第二份组件白名单。
|
||||
- 生成结果的稳定性要求:同一输入重复生成时,产物内容与文件时间戳不发生变化。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 接入 dev 与发布入口(下一里程碑)。
|
||||
- 移除构建脚本里的资源写入分支。
|
||||
- 编辑器分支产物(Unity/Godot/Cocos)的生成方式与外部工具链调用。
|
||||
- 运行时资源解析顺序、完整性校验语义与打包配置里的资源映射。
|
||||
- Linux 产物支持。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 主规范第 4.3 与第 4.4 节的合同(准备步骤合同、构建脚本退化后的职责边界)。
|
||||
- 现有随包资源与清单已由当前实现产出,可用于校验回归。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 资源合规时,校验路径可独立运行并通过,不依赖构建脚本的写入分支。
|
||||
- [ ] 上游锁定版本、平台、逐文件摘要、必需组件四类不一致各自被拒绝,并给出可定位的原因。
|
||||
- [ ] 重复执行资源生成,产物内容与文件时间戳不变(幂等)。
|
||||
- [ ] 同一份布局与组件白名单只有一处声明,构建期校验与准备步骤共用。
|
||||
|
||||
## 证据要求
|
||||
|
||||
- 自动化:资源校验的通过/拒绝用例;同输入重复生成后目录快照对比(内容 + 时间戳)。
|
||||
- 运行时:本机在既有随包资源上运行一次校验与一次构建,确认资源被正常读取且构建行为与改造前一致。
|
||||
- 边界:目标平台不支持、上游缺失、摘要不匹配、目录被非本工具内容占用四种场景各自的失败输出。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user