b5a143c04f
- 合并 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、共享决策日志与踩坑记录
206 lines
8.5 KiB
Rust
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()
|
|
}
|