forked from bai/curriculum-project-hub
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>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
//! Embedded `cph-render` package — makes an installed `cph` self-contained.
|
||||
//!
|
||||
//! The typst [`crate::world::LessonWorld`] reads the render package
|
||||
//! (`@local/cph-render` + the vendored `@preview/numbly`) **from disk**, by
|
||||
//! `render_dir.join(vpath.realize)`. In a source checkout that dir is the repo's
|
||||
//! `render/`. But a `cargo install`-ed binary has no repo around it — the old
|
||||
//! `env!("CARGO_MANIFEST_DIR")/../../render` path points into the build sandbox
|
||||
//! and does not exist at runtime.
|
||||
//!
|
||||
//! So we **embed the whole `render/` tree into the binary** at compile time
|
||||
//! (`include_dir!`) and, on first use, **extract it to a per-user cache dir**
|
||||
//! keyed by the crate version. `default_render_dir` returns that extracted path.
|
||||
//! The tree is tiny (~140 KiB, ~33 files), so embedding is cheap and extraction
|
||||
//! is a one-time cost per version.
|
||||
//!
|
||||
//! `render/vendor/local-packages/` (a dev-only self-referential symlink,
|
||||
//! gitignored) is excluded by the build script when it stages the tree for
|
||||
//! embedding — see `build.rs`. The embedded copy never contains it, which is
|
||||
//! correct: the World resolves `@local/cph-render` from `render_dir` directly,
|
||||
//! and `@preview/*` from `vendor/typst-packages/`, neither via that symlink.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use include_dir::{include_dir, Dir};
|
||||
|
||||
/// The `render/` tree, embedded at compile time from the **staged** copy the
|
||||
/// build script wrote to `OUT_DIR/render` (which omits the dev-only symlink that
|
||||
/// would otherwise make `include_dir!` recurse forever). `CPH_STAGED_RENDER_DIR`
|
||||
/// is set by `build.rs`.
|
||||
static RENDER_DIR: Dir<'_> = include_dir!("$CPH_STAGED_RENDER_DIR");
|
||||
|
||||
/// Resolve the directory the typst World should read the render package from,
|
||||
/// extracting the embedded copy to a per-user cache dir if needed.
|
||||
///
|
||||
/// Resolution order:
|
||||
/// 1. `CPH_RENDER_DIR` env var — an explicit override (dev convenience: point
|
||||
/// at the live repo `render/`).
|
||||
/// 2. The extracted embedded copy under the user cache dir
|
||||
/// (`<cache>/cph/render-<version>/`). Extracted once per crate version;
|
||||
/// subsequent runs reuse it.
|
||||
///
|
||||
/// On any failure to locate a cache dir or extract, falls back to a temp-dir
|
||||
/// location so the engine still works (just re-extracting per process).
|
||||
pub fn resolve_render_dir() -> PathBuf {
|
||||
if let Ok(dir) = std::env::var("CPH_RENDER_DIR") {
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
ensure_extracted().unwrap_or_else(|_| {
|
||||
// Last-resort: extract under the OS temp dir. Still correct, just not
|
||||
// cached across processes.
|
||||
let fallback = std::env::temp_dir().join(format!("cph-render-{}", env!("CARGO_PKG_VERSION")));
|
||||
let _ = extract_to(&fallback);
|
||||
fallback
|
||||
})
|
||||
}
|
||||
|
||||
/// The version-keyed cache location and a guarantee the embedded tree is present
|
||||
/// there. Returns the directory the World should use.
|
||||
fn ensure_extracted() -> std::io::Result<PathBuf> {
|
||||
let base = dirs::cache_dir()
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no user cache dir"))?;
|
||||
let dest = base
|
||||
.join("cph")
|
||||
.join(format!("render-{}", env!("CARGO_PKG_VERSION")));
|
||||
|
||||
// A sentinel marks a complete extraction; if present, reuse as-is. (Keyed by
|
||||
// version, so a new `cph` version re-extracts into a fresh dir.)
|
||||
let sentinel = dest.join(".extracted");
|
||||
if sentinel.is_file() {
|
||||
return Ok(dest);
|
||||
}
|
||||
|
||||
extract_to(&dest)?;
|
||||
std::fs::write(&sentinel, env!("CARGO_PKG_VERSION"))?;
|
||||
Ok(dest)
|
||||
}
|
||||
|
||||
/// Write the embedded `render/` tree under `dest` (creating dirs as needed).
|
||||
///
|
||||
/// Each embedded entry's `.path()` is already relative to the embed root (e.g.
|
||||
/// `src/elements/segment.typ`), so we join the **full** path under `dest` and
|
||||
/// create parents — no per-level mirroring needed.
|
||||
fn extract_to(dest: &Path) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(dest)?;
|
||||
write_dir(&RENDER_DIR, dest)
|
||||
}
|
||||
|
||||
/// Recursively write an embedded [`Dir`]'s files under `dest`, preserving the
|
||||
/// full relative path of each file.
|
||||
fn write_dir(dir: &Dir<'_>, dest: &Path) -> std::io::Result<()> {
|
||||
for entry in dir.entries() {
|
||||
match entry {
|
||||
include_dir::DirEntry::Dir(sub) => write_dir(sub, dest)?,
|
||||
include_dir::DirEntry::File(file) => {
|
||||
let file_dest = dest.join(file.path());
|
||||
if let Some(parent) = file_dest.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(&file_dest, file.contents())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+13
-11
@@ -35,6 +35,7 @@
|
||||
//! VirtualPath)`, and a diagnostic span is a `DiagSpan` (not a bare `Span`).
|
||||
|
||||
mod diag;
|
||||
mod embedded;
|
||||
mod manifest;
|
||||
mod world;
|
||||
|
||||
@@ -68,8 +69,9 @@ pub struct Engine {
|
||||
impl Engine {
|
||||
/// Construct an engine, discovering system + embedded fonts. The render
|
||||
/// package directory is taken from the `CPH_RENDER_DIR` environment variable
|
||||
/// when set, otherwise defaults to `<repo>/render` resolved relative to this
|
||||
/// crate (`CARGO_MANIFEST_DIR/../../render`).
|
||||
/// when set, otherwise the **embedded** render package is extracted to a
|
||||
/// per-user cache dir and used (see [`default_render_dir`]). This keeps an
|
||||
/// installed `cph` self-contained — no source checkout required at runtime.
|
||||
pub fn new() -> Self {
|
||||
Self::with_render_dir(default_render_dir())
|
||||
}
|
||||
@@ -234,14 +236,14 @@ fn map_all(world: &LessonWorld, diags: &[typst::diag::SourceDiagnostic]) -> Vec<
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `<crate>/../../render` — the repo's render package, used when `CPH_RENDER_DIR`
|
||||
/// is unset.
|
||||
/// The render-package directory the engine resolves `@local/cph-render` (and the
|
||||
/// vendored `@preview/*`) from when no explicit dir is given.
|
||||
///
|
||||
/// Honors `CPH_RENDER_DIR` first (dev override → live repo `render/`); otherwise
|
||||
/// returns a per-user cache dir holding the **embedded** render tree, extracted
|
||||
/// on demand (see [`embedded`]). This is what makes a `cargo install`-ed `cph`
|
||||
/// self-contained: it no longer depends on a source checkout living at a
|
||||
/// compile-time path.
|
||||
fn default_render_dir() -> PathBuf {
|
||||
if let Ok(dir) = std::env::var("CPH_RENDER_DIR") {
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("render")
|
||||
embedded::resolve_render_dir()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user