27de28a487
根目录统一管理九个 workspace 与唯一 lockfile 迁移 CI、Jenkins 和容器的根目录单次 npm ci 补齐 hoist 兼容、版本校验与 workspace 门禁 同步依赖边界方案、运维文档和长期记忆
206 lines
7.7 KiB
Rust
206 lines
7.7 KiB
Rust
#[path = "build_support/frontend_dist_guard.rs"]
|
||
mod frontend_dist_guard;
|
||
#[path = "build_support/runtime_prompt_bundle.rs"]
|
||
mod runtime_prompt_bundle;
|
||
|
||
#[cfg(windows)]
|
||
use sha2::{Digest, Sha256};
|
||
use std::collections::BTreeSet;
|
||
use std::env;
|
||
use std::fs;
|
||
use std::path::PathBuf;
|
||
|
||
#[cfg(windows)]
|
||
use std::io::{BufReader, Read};
|
||
|
||
const BUNDLED_CODEX_CLI_VERSION: &str = "codex-cli 0.147.0";
|
||
|
||
#[cfg(windows)]
|
||
const BUNDLED_CODEX_FILES: [&str; 6] = [
|
||
"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",
|
||
];
|
||
|
||
#[cfg(windows)]
|
||
fn sha256_file(path: &std::path::Path) -> Result<String, std::io::Error> {
|
||
let file = fs::File::open(path)?;
|
||
let mut reader = BufReader::new(file);
|
||
let mut hasher = Sha256::new();
|
||
let mut buffer = [0_u8; 64 * 1024];
|
||
loop {
|
||
let read = reader.read(&mut buffer)?;
|
||
if read == 0 {
|
||
break;
|
||
}
|
||
hasher.update(&buffer[..read]);
|
||
}
|
||
Ok(format!("{:x}", hasher.finalize()))
|
||
}
|
||
|
||
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||
#[cfg(windows)]
|
||
{
|
||
let app_root = manifest_dir
|
||
.parent()
|
||
.expect("AI 游戏创作 Tauri manifest 必须位于应用目录下");
|
||
let repo_root = app_root
|
||
.parent()
|
||
.and_then(|apps_dir| apps_dir.parent())
|
||
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
|
||
let source_candidates = [
|
||
app_root.join(
|
||
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||
),
|
||
app_root.join(
|
||
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||
),
|
||
repo_root.join(
|
||
"node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||
),
|
||
repo_root.join(
|
||
"node_modules/@openai/codex/node_modules/@openai/codex-win32-x64/vendor/x86_64-pc-windows-msvc",
|
||
),
|
||
];
|
||
let source = source_candidates
|
||
.iter()
|
||
.find(|path| {
|
||
BUNDLED_CODEX_FILES
|
||
.iter()
|
||
.all(|relative| path.join(relative).is_file())
|
||
})
|
||
.cloned()
|
||
.unwrap_or_else(|| {
|
||
panic!(
|
||
"内置 Codex CLI 缺失;请先在仓库根目录执行 npm ci(已检查:{})",
|
||
source_candidates
|
||
.iter()
|
||
.map(|path| path.display().to_string())
|
||
.collect::<Vec<_>>()
|
||
.join(";")
|
||
)
|
||
});
|
||
let target_dir = manifest_dir.join("resources/codex/win-x64");
|
||
let notice = target_dir.join("NOTICE.md");
|
||
if !notice.is_file() {
|
||
panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display());
|
||
}
|
||
fs::create_dir_all(&target_dir).expect("创建内置 Codex CLI 资源目录失败");
|
||
let mut file_hashes = serde_json::Map::new();
|
||
for relative in BUNDLED_CODEX_FILES {
|
||
let source_path = source.join(relative);
|
||
let target_path = target_dir.join(relative);
|
||
if let Some(parent) = target_path.parent() {
|
||
fs::create_dir_all(parent).expect("创建内置 Codex CLI 资源子目录失败");
|
||
}
|
||
let source_sha256 = sha256_file(&source_path).expect("读取内置 Codex CLI 资源失败");
|
||
let target_matches_source = target_path.is_file()
|
||
&& sha256_file(&target_path)
|
||
.map(|target_sha256| target_sha256 == source_sha256)
|
||
.unwrap_or(false);
|
||
if !target_matches_source {
|
||
fs::copy(&source_path, &target_path).expect("复制内置 Codex CLI 资源失败");
|
||
}
|
||
file_hashes.insert(
|
||
relative.to_string(),
|
||
serde_json::Value::String(source_sha256),
|
||
);
|
||
}
|
||
let manifest = serde_json::json!({
|
||
"schemaVersion": "genarrative-codex-sidecar.v2",
|
||
"platform": "win32-x64",
|
||
"version": BUNDLED_CODEX_CLI_VERSION,
|
||
"files": file_hashes,
|
||
});
|
||
let manifest_path = target_dir.join("manifest.json");
|
||
let manifest_payload = format!(
|
||
"{}\n",
|
||
serde_json::to_string_pretty(&manifest).expect("序列化内置 Codex CLI 清单失败")
|
||
);
|
||
if fs::read_to_string(&manifest_path)
|
||
.map(|current| current != manifest_payload)
|
||
.unwrap_or(true)
|
||
{
|
||
fs::write(&manifest_path, manifest_payload).expect("写入内置 Codex CLI 清单失败");
|
||
}
|
||
for relative in BUNDLED_CODEX_FILES {
|
||
println!("cargo:rerun-if-changed={}", source.join(relative).display());
|
||
}
|
||
println!("cargo:rerun-if-changed={}", notice.display());
|
||
}
|
||
}
|
||
|
||
fn seed_task_group_id(
|
||
group: &shared_contracts::game_creation_app::GameCreationAppAgentGroup,
|
||
) -> &'static str {
|
||
use shared_contracts::game_creation_app::GameCreationAppAgentGroup;
|
||
match group {
|
||
GameCreationAppAgentGroup::Design => "design",
|
||
GameCreationAppAgentGroup::Art => "art",
|
||
GameCreationAppAgentGroup::Code => "code",
|
||
GameCreationAppAgentGroup::Balance => "balance",
|
||
GameCreationAppAgentGroup::Audio => "audio",
|
||
GameCreationAppAgentGroup::Publishing => "publishing",
|
||
}
|
||
}
|
||
|
||
fn validate_seed_task_catalog(compiled: &runtime_prompt_bundle::CompiledPromptBundle) {
|
||
let actual = compiled
|
||
.specialist_nodes
|
||
.iter()
|
||
.map(|node| {
|
||
(
|
||
node.task_id.clone(),
|
||
node.group_id.clone(),
|
||
node.role.clone(),
|
||
)
|
||
})
|
||
.collect::<BTreeSet<_>>();
|
||
let expected = shared_contracts::game_creation_app::new_game_creation_app_seed_tasks()
|
||
.into_iter()
|
||
.map(|task| {
|
||
(
|
||
task.id,
|
||
seed_task_group_id(&task.group).to_string(),
|
||
task.role,
|
||
)
|
||
})
|
||
.collect::<BTreeSet<_>>();
|
||
if actual != expected {
|
||
panic!(
|
||
"Prompt Bundle agentCatalog 与正式 seed DAG 的 taskId/group/role 不一致\nactual={actual:#?}\nexpected={expected:#?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let manifest_dir = PathBuf::from(
|
||
env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be available"),
|
||
);
|
||
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
|
||
stage_bundled_codex_cli(&manifest_dir);
|
||
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
|
||
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
|
||
validate_seed_task_catalog(&compiled);
|
||
for dependency in &compiled.dependencies {
|
||
println!("cargo:rerun-if-changed={}", dependency.display());
|
||
}
|
||
let output_path = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be available"))
|
||
.join("agent_runtime_prompt_bundle.rs");
|
||
fs::write(&output_path, compiled.rust_source)
|
||
.unwrap_or_else(|error| panic!("写入 Prompt Bundle 生成代码失败:{error}"));
|
||
if !tauri_build::is_dev() {
|
||
let frontend_dist = manifest_dir
|
||
.parent()
|
||
.expect("AI 游戏创作 Tauri manifest 必须位于应用目录下")
|
||
.join("dist");
|
||
println!("cargo:rerun-if-changed={}", frontend_dist.display());
|
||
frontend_dist_guard::validate_frontend_dist(&frontend_dist)
|
||
.unwrap_or_else(|error| panic!("生产 frontendDist 检查失败:{error}"));
|
||
}
|
||
tauri_build::build()
|
||
}
|