// 构建脚本只用布局里的目录与校验入口(写入分支已移交准备步骤), // 其余字段与常量供运行期使用,因此这里不报构建上下文里的 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::>(); 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::>(); 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() }