Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 array
  • detect_and_get_active_variant(default_index) scans Cargo features
  • extra_hash(entry.as_str()) forces re-export when the variant changes
  • ExportConfig::with_variant(*entry) creates variant-specific config
  • write_variant_info() generates OUT_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 defaults
  • export_glyphs_by_name_map() - uses external name→codepoint mapping
  • No extra_hash needed (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::Map with GLYPHS (char → name) and REVERSE_GLYPHS (name → char) maps
  • icons.rs - pub const strings for each glyph name
  • variant.rs - GRESOURCE_PREFIX and GLYPH_PREFIX constants

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.