Build Script & Runtime API Patterns
Each font family crate follows a consistent structure with a build.rs
for build-time glyph export and a lib.rs for runtime API. This page
documents the two main build.rs patterns and the standard lib.rs
layout.
Crate File Structure
crates/fonts-rs-{name}/
├── Cargo.toml
├── build.rs
├── resources/
│ ├── {font}.ttf or .otf
│ ├── {license}.txt
│ └── icons.gresource.xml (generated by build.rs)
└── src/
├── lib.rs
├── definition.rs
├── naming.rs
├── variant.rs (generated by build.rs)
├── codepoint_map.rs (generated by build.rs)
├── constants.rs (generated by build.rs)
└── fonts.rs (optional, render feature)
definition.rs - FontFamilyConfig Implementation
Each font family crate defines a definition.rs with a
FontFamilyConfig implementation:
#![allow(unused)]
fn main() {
use const_format::concatcp;
use fonts_rs_generator::FontFamilyConfig;
use fonts_rs_model::CodePointRange;
use fonts_rs_model::GRESOURCE_BASE_PREFIX;
use fonts_rs_model::ASCII_PRINTABLE_RANGE;
pub struct DotoConfig;
impl FontFamilyConfig for DotoConfig {
const FONT_FAMILY_NAME: &'static str = "doto";
const FAMILY_DISPLAY_NAME: &'static str = "Doto";
const GRESOURCE_PREFIX: &'static str = concatcp!(GRESOURCE_BASE_PREFIX, "/", DotoConfig::FONT_FAMILY_NAME);
const CODEPOINT_RANGES: &[CodePointRange] = ASCII_PRINTABLE_RANGE;
}
}
The definition.rs is included by both build.rs (via #[path = ...])
and lib.rs (via mod definition).
build.rs Patterns
There are two main patterns for build.rs, depending on whether the font
has variants.
Pattern 1: Variable Font with Variants (e.g. Doto)
Used for variable fonts where the same TTF file is rendered at different axis locations, or for font families with multiple separate font files. Variants are selected via Cargo features at build time.
use std::path::Path;
use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::VariantList;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::AxisValue;
use fonts_rs_model::FontVariant;
#[path = "src/definition.rs"]
mod definition;
use definition::DotoConfig;
const FONT_FILE: &str = "Doto.ttf";
const VARIANTS: VariantList<DotoConfig> = VariantList::new(&[
FontVariant::axes("regular-medium", &[AxisValue::new("wght", 500.0), AxisValue::new("ROND", 50.0)]),
FontVariant::axes("bold-dot", &[AxisValue::new("wght", 700.0), AxisValue::new("ROND", 100.0)]),
// ...
]);
fn main() -> miette::Result<()> {
let entry = VARIANTS.detect_and_get_active_variant(12)?;
let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
eprintln!("build.rs: active variant: {} -> {}", entry, font_path.display());
set_font_path_env("DOTO_FONT_PATH", &font_path)?;
let config = ExportConfig::<DotoConfig>::with_variant(*entry);
FontBuild::new(&font_path)
.extra_hash(entry.as_str())
.run(|font_path, resources_dir| config.export_glyphs(font_path, resources_dir))
.map_err(|e| miette::miette!("{e}"))?;
config.write_variant_info()?;
Ok(())
}
Key points:
VariantList::new(&[...])defines all variants as a const arraydetect_and_get_active_variant(default_index)scans Cargo featuresextra_hash(entry.as_str())forces re-export when the variant changesExportConfig::with_variant(*entry)creates variant-specific configwrite_variant_info()generatesOUT_DIR/variant.rs
Pattern 2: Static Font with External Name Map (e.g. Bravura)
Used for fonts without PostScript glyph names (e.g. SMuFL fonts with
post table version 3.0). Glyph names come from an external metadata
file (e.g. glyphnames.json).
use std::path::Path;
use fonts_rs_generator::ExportConfig;
use fonts_rs_generator::FontBuild;
use fonts_rs_generator::build_constants;
use fonts_rs_generator::set_font_path_env;
use fonts_rs_model::GlyphNameMap;
#[path = "src/definition.rs"]
mod definition;
use definition::BravuraConfig;
const FONT_FILE: &str = "Bravura.otf";
const GLYPHNAMES_FILE: &str = "glyphnames.json";
fn parse_glyphnames(json: &str) -> miette::Result<GlyphNameMap> {
serde_json::from_str(json).map_err(|e| miette::miette!("Failed to parse glyphnames.json: {e}"))
}
fn main() -> miette::Result<()> {
let font_path = Path::new(build_constants::RESOURCES_DIR).join(FONT_FILE);
let glyphnames_path = Path::new(build_constants::RESOURCES_DIR).join(GLYPHNAMES_FILE);
eprintln!("build.rs: exporting glyphs from {}", font_path.display());
set_font_path_env("BRAVURA_FONT_PATH", &font_path)?;
let glyphnames_json = std::fs::read_to_string(&glyphnames_path)
.map_err(|e| miette::miette!("Failed to read {}: {e}", glyphnames_path.display()))?;
let name_map = parse_glyphnames(&glyphnames_json)?;
eprintln!("build.rs: parsed {} SMuFL glyph names", name_map.len());
let config = ExportConfig::<BravuraConfig>::new();
FontBuild::new(&font_path)
.run(|font_path, resources_dir| {
config.export_glyphs_by_name_map(font_path, resources_dir, &name_map)
})
.map_err(|e| miette::miette!("{e}"))?;
config.write_variant_info()?;
Ok(())
}
Key points:
ExportConfig::new()- no variant, uses family config defaultsexport_glyphs_by_name_map()- uses external name→codepoint mapping- No
extra_hashneeded (no variant selection)
lib.rs - Runtime API
Each font family crate has a lib.rs with the following structure:
#![allow(unused)]
fn main() {
pub mod codepoint_map; // include!(concat!(env!("OUT_DIR"), "/codemap.rs"))
pub mod constants; // include!(concat!(env!("OUT_DIR"), "/icons.rs"))
pub mod naming; // FontFamily marker + GlyphName<F> type alias
pub mod variant; // include!(concat!(env!("OUT_DIR"), "/variant.rs"))
#[cfg(feature = "render")]
pub mod fonts; // impl_font_loader!(env: "FONT_PATH")
use fonts_rs_model::CodePoint;
// Re-export key types
pub use naming::DotoName;
// GResource registration (gtk feature)
#[cfg(feature = "gtk")]
pub fn register_glyphs() -> Result<(), gio::glib::Error> {
gio::resources_register_include!("icons.gresource")?;
Ok(())
}
// Codepoint lookup
pub trait GlyphNameExt {
fn codepoint(&self) -> Option<CodePoint>;
}
impl GlyphNameExt for DotoName {
fn codepoint(&self) -> Option<CodePoint> {
let key = self.as_ref();
if let Some(ch) = codepoint_map::REVERSE_GLYPHS.get(key).copied() {
return Some(CodePoint::from(ch));
}
let full = format!("{}-{}", variant::GLYPH_PREFIX, key);
codepoint_map::REVERSE_GLYPHS.get(&full).copied().map(CodePoint::from)
}
}
// Iterator over all glyphs
pub fn all_glyphs() -> impl Iterator<Item = (char, &'static str)> {
codepoint_map::GLYPHS.entries().map(|(c, name)| (*c, *name))
}
}
Generated Modules
Three modules are generated by build.rs into OUT_DIR:
codepoint_map.rs-phf::MapwithGLYPHS(char → name) andREVERSE_GLYPHS(name → char) mapsicons.rs-pub conststrings for each glyph namevariant.rs-GRESOURCE_PREFIXandGLYPH_PREFIXconstants
naming.rs - Font Family Marker
#![allow(unused)]
fn main() {
use fonts_rs_model::FontFamily;
use fonts_rs_model::GlyphName;
use fonts_rs_model::sealed;
pub enum Doto {}
impl FontFamily for Doto {}
impl sealed::Sealed for Doto {}
pub type DotoName = GlyphName<Doto>;
}
variant.rs - Generated Variant Info
#![allow(unused)]
fn main() {
// @generated by build.rs - do not edit
/// GResource prefix for Doto.
pub const GRESOURCE_PREFIX: &str = "/io/smearor/fonts/doto/regular_medium";
/// Glyph name prefix for Doto.
pub const GLYPH_PREFIX: &str = "doto-regular-medium";
}
fonts.rs - Font Loading (Optional)
#![allow(unused)]
fn main() {
fonts_rs_generator::impl_font_loader!(env: "DOTO_FONT_PATH");
}
Generates a font() function that loads the TTF file from the path
set by build.rs via cargo:rustc-env. Cached in OnceLock.