Files
curriculum-project-hub/crates/cph-typst/build.rs
sjfhsjfh 9590e15236 feat(checker+typst): 嵌入 render 使 cph 可 cargo install;实现 shell step 执行器(ADR-0013)
三件事,服务于"把恒一小奥 KenKen 课作者化为合法 native 工程文件"这条主线
(工程文件本身在 repo 外的 sibling playground,不入此提交):

1. cph-typst: 把 render/ 编译期嵌入二进制,`cargo install` 后开箱即用
   - build.rs 把 render/ 拷进 OUT_DIR,剔除 dev 自指符号链接 vendor/local-packages
     (否则 include_dir! 无限递归)
   - embedded.rs 用 include_dir! 嵌入,运行时按版本解压到 per-user cache dir
     (CPH_RENDER_DIR 仍优先作 dev override)
   - default_render_dir 不再用编译期 CARGO_MANIFEST_DIR 写死路径

2. cph-check + cph-cli: 实现 Step::Shell 执行器(此前仅占位)
   - run_shell_target: 跑 check 门控 → 以工程根为 cwd 执行 shell step,首非零退出即停
   - target_is_shell + CLI run_shell_build 路由;`check` 不跑 shell(结构校验只看 lesson)
   - compile_targets 只保留含 TypstCompile 的 target,纯 shell target 不走 typst 引擎
   - 语义:失败属 build-过程错误,不入 6 类诊断(taxonomy 保持 6,显式拒绝加 ShellStep 码)

3. spec + ADR: 在 Export/Render.lean 钉 shell step 执行语义 prose;
   ADR-0013(已实现)、ADR-0014(md/HTML 导出后端方向,Proposed/设计先行,未实现)

测试:全 workspace 42 passed(新增 4 个 shell 执行器测试);lake build 25 jobs 绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:09:49 +08:00

65 lines
2.8 KiB
Rust

//! Build script: stage a clean copy of the repo `render/` package into `OUT_DIR`
//! so `src/embedded.rs` can `include_dir!` it without tripping over the dev-only
//! self-referential symlink at `render/vendor/local-packages/`.
//!
//! Why this exists: `include_dir!` walks a directory at compile time and follows
//! symlinks. The repo keeps `render/vendor/local-packages/local/cph-render/0.1.0`
//! as a symlink pointing back at `render/` (a gitignored dev convenience for
//! typst's local-package resolution). Embedding `render/` directly would make
//! `include_dir!` recurse into that link forever. The runtime never needs that
//! link (the engine resolves `@local/cph-render` from the render dir directly,
//! and `@preview/*` from `vendor/typst-packages/`), so we copy everything EXCEPT
//! `vendor/local-packages/` into `OUT_DIR/render` and embed that staged tree.
use std::path::{Path, PathBuf};
fn main() {
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let render_src = manifest_dir.join("..").join("..").join("render");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let staged = out_dir.join("render");
// Rebuild the staged tree from scratch each run (cheap — ~140 KiB).
if staged.exists() {
let _ = std::fs::remove_dir_all(&staged);
}
copy_tree(&render_src, &staged).expect("stage render/ for embedding");
// Re-run when the render package changes.
println!("cargo:rerun-if-changed={}", render_src.display());
// Expose the staged path to the crate via an env var include_dir! can read.
println!("cargo:rustc-env=CPH_STAGED_RENDER_DIR={}", staged.display());
}
/// Recursively copy `src` → `dst`, skipping the dev-only `vendor/local-packages`
/// subtree and never following symlinks into it.
fn copy_tree(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let name = entry.file_name();
let from = entry.path();
let to = dst.join(&name);
// Skip the self-referential dev symlink tree.
if from.ends_with("vendor/local-packages") || name == *"local-packages" {
continue;
}
// Resolve symlinks/metadata without following into a cycle: use
// `symlink_metadata` so a symlinked dir is treated by its link, not its
// (recursive) target. We only copy real files and real directories.
let meta = std::fs::symlink_metadata(&from)?;
let ft = meta.file_type();
if ft.is_symlink() {
// Skip any symlink (the only one in the tree is the dev convenience).
continue;
} else if ft.is_dir() {
copy_tree(&from, &to)?;
} else if ft.is_file() {
std::fs::copy(&from, &to)?;
}
}
Ok(())
}