forked from EduCraft/curriculum-project-hub
106 lines
4.5 KiB
Rust
106 lines
4.5 KiB
Rust
//! 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(())
|
|
}
|