Files
Genarrative/apps/ai-game-creator-shell/src-tauri/build.rs
T
suzmii b5a143c04f 合并 origin/master:随包资源仍由准备步骤生成,Claude Agent SDK sidecar 按同一架构归位
- 合并 origin/master(140 个提交),解决 4 处冲突:build.rs 取只读校验分支、package.json 同时保留 bundled-resources:test 与 check:generated-bindings、决策日志与踩坑记录两侧条目并存
- master 的构建期 staging(stage_bundled_claude_agent、prune_stale_codex_components)不进构建脚本;sidecar 改由声明与准备步骤产出
- package-layout.json 新增 claudeAgent section(锁定版本、sidecar 入口、上游 SDK 与平台原生运行时目标表、复制跳过规则),layoutVersion 递增到 2
- prepare-bundled-resources.mjs 新增 sidecar 整目录原子替换与缓存 key,复制规则按 section 参数化,Windows 真机 24 个文件、复跑命中缓存零写入
- package_layout.rs 新增 sidecar 只读校验(入口与仓库源码逐字节一致、上游包版本、可执行位、白名单外文件)与声明钉住用例
- build.rs 调用 sidecar 校验并导出 AGC_CLAUDE_AGENT_SDK_VERSION;claude_code_cli.rs 的 sidecar 身份串改用声明版本
- resources/claude-agent 映射从基线 tauri.conf.json 移到 windows/macos 平台配置,check-config.mjs 增加「基线不得声明 resources/**」守卫
- 修正 package_layout 的 Windows 路径期望用例(期望值改与实现同形,原用例在 Windows 必红)
- 同步技术方案 §4.5/§4.9、共享决策日志与踩坑记录
2026-09-30 17:23:04 +08:00

206 lines
8.5 KiB
Rust

// 构建脚本只用布局里的目录与校验入口(写入分支已移交准备步骤),
// 其余字段与常量供运行期使用,因此这里不报构建上下文里的 dead_code。
#[allow(dead_code)]
#[path = "build_support/codex_bundle.rs"]
mod codex_bundle;
#[path = "build_support/frontend_dist_guard.rs"]
mod frontend_dist_guard;
#[path = "build_support/godot_bundle.rs"]
mod godot_bundle;
#[path = "build_support/runtime_prompt_bundle.rs"]
mod runtime_prompt_bundle;
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::PathBuf;
use codex_bundle::package_layout;
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 validate_staged_resources(manifest_dir: &std::path::Path) {
let target = env::var("TARGET").expect("Cargo TARGET");
for staged_target in package_layout::staged_targets(&target) {
let Some(layout) = codex_bundle::for_target(staged_target) else {
continue;
};
let target_dir = manifest_dir
.join(package_layout::codex().resource_directory)
.join(layout.directory);
package_layout::validate_staged_codex_bundle(&target_dir, staged_target).unwrap_or_else(
|error| panic!("内置 Codex CLI 随包资源校验失败({staged_target}):{error}"),
);
}
validate_staged_claude_agent(manifest_dir, &target);
validate_staged_plugin_workspace(manifest_dir, &target);
validate_prepared_payloads(manifest_dir, &target);
}
/// 只读校验 Claude Agent SDK sidecar:入口、SDK 与平台原生运行时必须在位。
/// 未声明该目标时不随包 sidecar,直接放行(Linux 等平台不参与客户端打包)。
fn validate_staged_claude_agent(manifest_dir: &std::path::Path, target: &str) {
if package_layout::claude_agent_target(target).is_none() {
return;
}
let app_root = manifest_dir
.parent()
.expect("AI 游戏创作 Tauri manifest 必须位于应用目录下");
let target_dir = manifest_dir.join(package_layout::claude_agent().resource_directory);
package_layout::validate_staged_claude_agent(&target_dir, app_root, target).unwrap_or_else(
|error| panic!("Claude Agent SDK sidecar 随包资源校验失败({target}):{error}"),
);
}
/// 只读校验插件随包工作区:声明的源码派生内容必须与仓库源码逐文件一致,整树无符号链接。
/// 已准备产物与随包库在本机无法重建,构建期至少要确认它们已经就位。
fn validate_prepared_payloads(manifest_dir: &std::path::Path, target: &str) {
if !package_layout::plugin_staging_applies(target) {
return;
}
let declared = package_layout::plugins();
let repo_root = manifest_dir
.parent()
.and_then(|app_root| app_root.parent())
.and_then(|apps_dir| apps_dir.parent())
.expect("AGC 应用必须位于仓库 apps 目录下");
let destination_root = manifest_dir.join(declared.destination_directory);
for plugin in package_layout::plugin_directories(
&repo_root.join(declared.source_directory),
declared.manifest_file_name,
)
.unwrap_or_else(|error| panic!("{error}"))
{
for subdirectory in declared.subdirectories {
if !package_layout::subdirectory_is_prepared(subdirectory)
|| !package_layout::subdirectory_applies_to_plugin(subdirectory, &plugin.name)
|| !package_layout::subdirectory_enabled(
subdirectory,
target,
package_layout::cargo_feature_enabled,
)
{
continue;
}
let relative = package_layout::declared_relative_path(subdirectory.path);
let staged = destination_root.join(&plugin.name).join(&relative);
if !staged.is_dir() {
panic!(
"随包已准备产物缺失:{}(请先执行随包资源准备步骤)",
staged.display()
);
}
}
for staging in declared.library_staging {
if plugin.name != staging.plugin
|| !package_layout::library_staging_enabled(
staging,
target,
package_layout::cargo_feature_enabled,
)
{
continue;
}
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
let staged = destination_root.join(&plugin.name).join(&relative);
godot_bundle::validate(&staged)
.unwrap_or_else(|error| panic!("Godot 随包资源校验失败:{error}"));
}
}
}
fn validate_staged_plugin_workspace(manifest_dir: &std::path::Path, target: &str) {
let declared = package_layout::plugins();
let repo_root = manifest_dir
.parent()
.and_then(|app_root| app_root.parent())
.and_then(|apps_dir| apps_dir.parent())
.expect("AGC 应用必须位于仓库 apps 目录下");
package_layout::validate_staged_plugins(
&repo_root.join(declared.source_directory),
&manifest_dir.join(declared.destination_directory),
target,
package_layout::cargo_feature_enabled,
)
.unwrap_or_else(|error| panic!("插件随包资源校验失败:{error}"));
}
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");
// 运行期定位随包目录依赖该编译期常量,与是否跳过 staging 无关(见技术方案 §4.4)。
println!(
"cargo:rustc-env=AGC_BUILD_TARGET={}",
env::var("TARGET").expect("Cargo TARGET")
);
// Claude Agent SDK 的版本只声明一处,运行期的 sidecar 身份串由此取值。
println!(
"cargo:rustc-env=AGC_CLAUDE_AGENT_SDK_VERSION={}",
package_layout::claude_agent().version
);
validate_staged_resources(&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()
}