构建脚本改为按声明生成并只读校验随包资源
- 平台布局、插件随包子目录与跳过规则、Codex 上游候选路径与第三方声明来源全部改由声明提供,删除脚本内的字面量 - 新增只读校验:Codex 目录校验清单 schema/平台/版本、文件集合、逐文件 sha256、第三方声明、可执行位与白名单外文件;插件目录校验必需组件与整树符号链接 - AGC_BUILD_TARGET 的 rustc-env 移到无条件路径(保留职责),新增 AGC_SKIP_RESOURCE_STAGING=1 以在既有产物上只跑校验 - 删除脚本内与 package_layout 重复的 sha256 实现,复用同一份
This commit is contained in:
@@ -15,35 +15,20 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::io::{BufReader, Read};
|
||||
|
||||
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()))
|
||||
}
|
||||
use codex_bundle::package_layout;
|
||||
|
||||
fn stage_bundled_codex_cli(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
println!("cargo:rustc-env=AGC_BUILD_TARGET={target}");
|
||||
if target.contains("apple-darwin") {
|
||||
let resource_directory = manifest_dir.join(package_layout::codex().resource_directory);
|
||||
if let Some(group) = package_layout::codex_universal_group(&target) {
|
||||
// Tauri 的 universal 两次 Cargo 编译共用 resource staging,
|
||||
// 每次都生成完整双架构目录,最终 bundle 不取决于最后编译的切片。
|
||||
let staging = manifest_dir.join("resources/codex/mac-native");
|
||||
let staging = resource_directory.join(group.directory);
|
||||
if staging.exists() {
|
||||
fs::remove_dir_all(&staging).expect("清理 macOS Codex staging 失败");
|
||||
}
|
||||
for target in ["aarch64-apple-darwin", "x86_64-apple-darwin"] {
|
||||
stage_codex_target(manifest_dir, target);
|
||||
for member in group.targets {
|
||||
stage_codex_target(manifest_dir, member);
|
||||
}
|
||||
} else {
|
||||
stage_codex_target(manifest_dir, &target);
|
||||
@@ -66,18 +51,9 @@ fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
|
||||
.parent()
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AI 游戏创作应用必须位于仓库 apps 目录下");
|
||||
let package = format!("codex-{}", layout.platform);
|
||||
let source_candidates = [app_root, repo_root]
|
||||
.into_iter()
|
||||
.flat_map(|root| {
|
||||
[
|
||||
root.join(format!("node_modules/@openai/{package}/vendor/{target}")),
|
||||
root.join(format!(
|
||||
"node_modules/@openai/codex/node_modules/@openai/{package}/vendor/{target}"
|
||||
)),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let source_candidates =
|
||||
package_layout::codex_source_candidates(app_root, repo_root, target)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
let source = source_candidates
|
||||
.iter()
|
||||
.find(|path| {
|
||||
@@ -98,18 +74,23 @@ fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
|
||||
)
|
||||
});
|
||||
let metadata: serde_json::Value = serde_json::from_slice(
|
||||
&fs::read(source.join("codex-package.json")).expect("读取 Codex 原生包元数据失败"),
|
||||
&fs::read(source.join(package_layout::codex().package_metadata_file_name))
|
||||
.expect("读取 Codex 原生包元数据失败"),
|
||||
)
|
||||
.expect("Codex 原生包元数据无效");
|
||||
codex_package_metadata::validate_package_metadata(&metadata, target, layout)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
let target_dir = manifest_dir.join("resources/codex").join(layout.directory);
|
||||
let notice = target_dir.join("NOTICE.md");
|
||||
if target.contains("apple-darwin") {
|
||||
let source_notice =
|
||||
manifest_dir.join("resources/codex/【声明】Mac内置Codex组件-2026-09-18.md");
|
||||
stage_plugin_file(&source_notice, ¬ice);
|
||||
println!("cargo:rerun-if-changed={}", source_notice.display());
|
||||
let target_dir = manifest_dir
|
||||
.join(package_layout::codex().resource_directory)
|
||||
.join(layout.directory);
|
||||
let notice = target_dir.join(package_layout::codex().notice_file_name);
|
||||
if let Some(notice_source) = package_layout::codex_notice_source(target) {
|
||||
if !notice_source.preserve {
|
||||
// 受版本控制的第三方声明由此处复制;`preserve` 的声明文件本身已在随包目录内。
|
||||
let source_notice = manifest_dir.join(notice_source.source);
|
||||
stage_plugin_file(&source_notice, ¬ice);
|
||||
println!("cargo:rerun-if-changed={}", source_notice.display());
|
||||
}
|
||||
}
|
||||
if !notice.is_file() {
|
||||
panic!("内置 Codex CLI 第三方声明缺失:{}", notice.display());
|
||||
@@ -122,9 +103,10 @@ fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
|
||||
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 source_sha256 =
|
||||
package_layout::sha256_file(&source_path).expect("读取内置 Codex CLI 资源失败");
|
||||
let target_matches_source = target_path.is_file()
|
||||
&& sha256_file(&target_path)
|
||||
&& package_layout::sha256_file(&target_path)
|
||||
.map(|target_sha256| target_sha256 == source_sha256)
|
||||
.unwrap_or(false);
|
||||
let source_permissions = fs::metadata(&source_path)
|
||||
@@ -156,7 +138,7 @@ fn stage_codex_target(manifest_dir: &std::path::Path, target: &str) {
|
||||
"version": codex_bundle::CLI_VERSION,
|
||||
"files": file_hashes,
|
||||
});
|
||||
let manifest_path = target_dir.join("manifest.json");
|
||||
let manifest_path = target_dir.join(package_layout::codex().manifest_file_name);
|
||||
let manifest_payload = format!(
|
||||
"{}\n",
|
||||
serde_json::to_string_pretty(&manifest).expect("序列化内置 Codex CLI 清单失败")
|
||||
@@ -217,16 +199,113 @@ fn validate_seed_task_catalog(compiled: &runtime_prompt_bundle::CompiledPromptBu
|
||||
}
|
||||
}
|
||||
|
||||
/// 只读校验:确认已经落盘的随包产物与声明一致。本函数不写任何文件。
|
||||
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_plugin_workspace(manifest_dir, &target);
|
||||
}
|
||||
|
||||
/// 只读校验插件随包工作区:插件清单与声明的必需子目录齐备、整树无符号链接、无越界路径。
|
||||
///
|
||||
/// 插件产物的逐文件摘要校验在准备步骤接管写入后启用——在那之前构建脚本仍会整体重建
|
||||
/// `resources/plugins`,任何写在树内的准备步骤清单都会被清掉。
|
||||
fn validate_staged_plugin_workspace(manifest_dir: &std::path::Path, target: &str) {
|
||||
if !package_layout::plugin_staging_applies(target) {
|
||||
return;
|
||||
}
|
||||
let declared = package_layout::plugins();
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
if !destination_root.is_dir() {
|
||||
panic!("插件随包资源目录缺失:{}", destination_root.display());
|
||||
}
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|app_root| app_root.parent())
|
||||
.and_then(|apps_dir| apps_dir.parent())
|
||||
.expect("AGC 应用必须位于仓库 apps 目录下");
|
||||
if let Ok(entries) = std::fs::read_dir(repo_root.join(declared.source_directory)) {
|
||||
for entry in entries.flatten() {
|
||||
let plugin_root = entry.path();
|
||||
if !plugin_root.is_dir() || !plugin_root.join(declared.manifest_file_name).is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let staged = destination_root.join(&name);
|
||||
if !staged.join(declared.manifest_file_name).is_file() {
|
||||
panic!(
|
||||
"随包插件缺少清单:{}",
|
||||
staged.join(declared.manifest_file_name).display()
|
||||
);
|
||||
}
|
||||
for subdirectory in declared.subdirectories {
|
||||
if !package_layout::subdirectory_enabled(
|
||||
subdirectory,
|
||||
target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
if plugin_root.join(&relative).is_dir() && !staged.join(&relative).is_dir() {
|
||||
panic!(
|
||||
"随包插件缺少必需目录:{}(插件 {})",
|
||||
staged.join(&relative).display(),
|
||||
name.to_string_lossy()
|
||||
);
|
||||
}
|
||||
}
|
||||
for staging in declared.library_staging {
|
||||
if 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);
|
||||
if !staged.join(&relative).is_dir() {
|
||||
panic!("随包库目录缺失:{}", staged.join(&relative).display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package_layout::collect_tree_files(&destination_root)
|
||||
.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");
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
stage_plugin_workspace(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
// 运行期定位随包目录依赖该编译期常量,与是否跳过 staging 无关(见技术方案 §4.4)。
|
||||
println!(
|
||||
"cargo:rustc-env=AGC_BUILD_TARGET={}",
|
||||
env::var("TARGET").expect("Cargo TARGET")
|
||||
);
|
||||
// AGC_SKIP_RESOURCE_STAGING=1 只做只读校验(要求随包资源已由准备步骤生成),
|
||||
// 用于在既有产物上单独验证校验路径。
|
||||
if env::var_os("AGC_SKIP_RESOURCE_STAGING").is_none() {
|
||||
stage_bundled_codex_cli(&manifest_dir);
|
||||
prepare_unity_editor_helper(&manifest_dir);
|
||||
prepare_godot_editor_extension(&manifest_dir);
|
||||
stage_plugin_workspace(&manifest_dir);
|
||||
stage_cocos_editor_payload(&manifest_dir);
|
||||
}
|
||||
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);
|
||||
@@ -435,17 +514,18 @@ fn prepare_godot_editor_extension(manifest_dir: &std::path::Path) {
|
||||
/// Cargo target 目录或 node_modules。
|
||||
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||
let target = env::var("TARGET").expect("Cargo TARGET");
|
||||
if !target.contains("windows") && !target.contains("apple-darwin") {
|
||||
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 目录下")
|
||||
.to_path_buf();
|
||||
let workspace = repo_root.join("plugins");
|
||||
let destination_root = manifest_dir.join("resources/plugins");
|
||||
let workspace = repo_root.join(declared.source_directory);
|
||||
let destination_root = manifest_dir.join(declared.destination_directory);
|
||||
// staging 是专用生成目录;重建清除跨目标 payload 与已删除插件的残留。
|
||||
if destination_root.exists() {
|
||||
std::fs::remove_dir_all(&destination_root).expect("清理插件 staging 失败");
|
||||
@@ -464,39 +544,47 @@ fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
|
||||
.is_symlink(),
|
||||
"插件工作区不允许符号链接"
|
||||
);
|
||||
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
|
||||
if !plugin_root.is_dir() || !plugin_root.join(declared.manifest_file_name).is_file() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let destination = destination_root.join(&name);
|
||||
copy_plugin_file(
|
||||
&plugin_root.join("plugin.json"),
|
||||
&destination.join("plugin.json"),
|
||||
&plugin_root.join(declared.manifest_file_name),
|
||||
&destination.join(declared.manifest_file_name),
|
||||
);
|
||||
for relative in [
|
||||
std::path::PathBuf::from("src"),
|
||||
std::path::PathBuf::from("panels"),
|
||||
std::path::PathBuf::from("skills"),
|
||||
std::path::PathBuf::from("native/payload"),
|
||||
std::path::PathBuf::from("dotnet/publish/win-x64"),
|
||||
] {
|
||||
if (relative == std::path::Path::new("native/payload") && !target.contains("windows"))
|
||||
|| (relative == std::path::Path::new("dotnet/publish/win-x64")
|
||||
&& (target != "x86_64-pc-windows-msvc"
|
||||
|| env::var_os("CARGO_FEATURE_UNITY_EDITOR_EXECUTE").is_none()))
|
||||
for subdirectory in declared.subdirectories {
|
||||
if !package_layout::subdirectory_enabled(
|
||||
subdirectory,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let relative = package_layout::declared_relative_path(subdirectory.path);
|
||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||
}
|
||||
for staging in declared.library_staging {
|
||||
if name != staging.plugin
|
||||
|| !package_layout::library_staging_enabled(
|
||||
staging,
|
||||
&target,
|
||||
package_layout::cargo_feature_enabled,
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
|
||||
}
|
||||
if name == "agc-godot-editor" {
|
||||
godot_bundle::stage(
|
||||
&plugin_root.join("native/gdextension"),
|
||||
&destination.join("native/gdextension"),
|
||||
&target,
|
||||
env::var_os("CARGO_FEATURE_GODOT_EDITOR_EXECUTE").is_some(),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
let relative = package_layout::declared_relative_path(staging.source_subdirectory);
|
||||
match staging.layout {
|
||||
"godot-bundle" => godot_bundle::stage(
|
||||
&plugin_root.join(&relative),
|
||||
&destination.join(&relative),
|
||||
&target,
|
||||
true,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}")),
|
||||
other => panic!("未实现的随包库 staging 布局:{other}"),
|
||||
}
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", plugin_root.display());
|
||||
}
|
||||
@@ -531,20 +619,15 @@ fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
|
||||
);
|
||||
if path.is_dir() {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with('.') || matches!(name.as_ref(), "target" | "node_modules") {
|
||||
if package_layout::skip_directory(&name.to_string_lossy()) {
|
||||
continue;
|
||||
}
|
||||
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
|
||||
copy_plugin_tree(&path, &target);
|
||||
} else {
|
||||
// 测试文件不随包分发。
|
||||
// 测试文件与隐藏文件不随包分发。
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.contains(".test.") {
|
||||
continue;
|
||||
}
|
||||
if name.starts_with('.') {
|
||||
if package_layout::skip_file_name(&name.to_string_lossy()) {
|
||||
continue;
|
||||
}
|
||||
stage_plugin_file(&path, &target);
|
||||
|
||||
Reference in New Issue
Block a user