//! 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 /// (`/cph/render-/`). 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 { 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(()) }