//! Optional build input: with `CRAFT_FONTS_DIR=`, embed every font //! in its `fonts/manifest.txt` as `CRAFT_FONTS` (see src/fonts.rs). Unset, `CRAFT_FONTS` is empty. //! Recipe from craft-fonts' docs/integration.md; craft-fonts is never a Cargo dependency, and this //! script only reads a local directory (no network). //! //! wasm32 (the web build) embeds only the UI font, BIZ UDPGothic Regular (~4.5 MB), to keep the //! `.wasm` within the web size budget (Cloudflare serves at most 25 MiB per file); the export //! watermark falls back to it there. Native builds embed every font. use std::fmt::Write as _; use std::path::{Path, PathBuf}; fn main() { println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-env-changed=CRAFT_FONTS_DIR"); println!("cargo::rerun-if-env-changed=CRAFT_FONTS_REQUIRED"); let required = std::env::var_os("CRAFT_FONTS_REQUIRED").is_some(); let mut src = String::from("pub static CRAFT_FONTS: &[CraftFont] = &[\n"); let mut embedded = 0; if let Some(dir) = std::env::var_os("CRAFT_FONTS_DIR").map(PathBuf::from).map(resolve) { match craft_fonts(&dir, required) { Ok(entries) => { embedded = entries.lines().count(); src.push_str(&entries); } Err(e) if required => { println!("cargo::error=CRAFT_FONTS_DIR={}: {e}", dir.display()); } Err(e) => println!("cargo::warning=building without craft-fonts: CRAFT_FONTS_DIR={}: {e}", dir.display()), } } else if required { println!("cargo::error=CRAFT_FONTS_REQUIRED is set but CRAFT_FONTS_DIR is missing"); } src.push_str("];\n"); // Without them the app still runs and every language still selectable, so Chinese and Japanese // just paint boxes — the one failure a user cannot diagnose from what they see. if embedded == 0 { println!( "cargo::warning=no craft-fonts embedded: Chinese and Japanese text will render as empty boxes. \ Clone https://github.com/storytold/craft-fonts next to this repo and rebuild with \ CRAFT_FONTS_DIR=../craft-fonts" ); } let out = PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("craft_fonts.rs"); if let Err(e) = std::fs::write(&out, src) { println!("cargo::error=writing {}: {e}", out.display()); } } /// Resolve a relative `CRAFT_FONTS_DIR` against both roots a caller might mean: a build script's /// cwd is the *package* root (`crates/engine`), while the `../craft-fonts` the README and /// craft-fonts' docs use is written relative to the *workspace* root and would otherwise resolve /// to `crates/craft-fonts`. fn resolve(dir: PathBuf) -> PathBuf { if dir.is_absolute() { return dir; } let package = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap_or_default()); let workspace = package.parent().and_then(Path::parent).unwrap_or(package.as_path()); let candidates = [workspace.join(&dir), package.join(&dir)]; candidates.iter().find(|candidate| candidate.is_dir()).cloned().unwrap_or_else(|| candidates[0].clone()) } /// One `CraftFont { .. }` initialiser per manifest line. fn craft_fonts(dir: &Path, required: bool) -> Result { let manifest = dir.join("fonts/manifest.txt"); println!("cargo::rerun-if-changed={}", manifest.display()); let text = std::fs::read_to_string(&manifest).map_err(|e| format!("{}: {e}", manifest.display()))?; let wasm = std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32"); let mut out = String::new(); let mut has_japanese_ui = false; let mut has_chinese_ui = false; for line in text.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) { let f: Vec<&str> = line.split(" | ").map(str::trim).collect(); let [family, style, file, scripts, ..] = f.as_slice() else { return Err(format!("malformed manifest line: {line}")); }; has_japanese_ui |= (*family, *style) == ("BIZ UDPGothic", "Regular"); has_chinese_ui |= (*family, *style) == ("Noto Sans CJK SC", "Regular") && scripts.split(',').any(|script| script.trim() == "Hans"); if wasm && (*family, *style) != ("BIZ UDPGothic", "Regular") { continue; } let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?; println!("cargo::rerun-if-changed={}", path.display()); let scripts: Vec = scripts.split(',').map(|s| format!("{:?}", s.trim())).collect(); let _ = writeln!( out, " CraftFont {{ family: {family:?}, style: {style:?}, scripts: &[{}], bytes: include_bytes!({:?}) }},", scripts.join(", "), path.display().to_string(), ); } if required && !has_japanese_ui { return Err("required Japanese UI face BIZ UDPGothic Regular is missing from fonts/manifest.txt".into()); } if required && !wasm && !has_chinese_ui { return Err("required Simplified Chinese UI face Noto Sans CJK SC Regular (Hans) is missing from fonts/manifest.txt".into()); } Ok(out) }