Something went wrong. Try again.
A warm humanist programming family in eighteen real faces, with proud operator gestures, controllable alternates, a 620-unit terminal grid,
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276#!/usr/bin/env python3"""Content-verified font stages; publish only outputs produced by this build."""
from __future__ import annotations
import argparsefrom contextlib import contextmanagerfrom dataclasses import dataclassimport fcntlimport hashlibimport jsonimport osimport platformfrom pathlib import Pathimport subprocessimport sysimport tempfile
from font_project import ROOT, STYLES
SCHEMA = 1KINDS = ("ttf", "woff2", "nerd")
@dataclass(frozen=True, slots=True)class Stage: style: str kind: str relative_path: str key: str
@dataclass(frozen=True, slots=True)class Plan: root: Path required: tuple[Stage, ...] pending: tuple[Stage, ...] receipts: dict[str, dict[str, str]]
def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for block in iter(lambda: handle.read(1 << 20), b""): digest.update(block) return digest.hexdigest()
def _digest(value: object) -> str: return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
def input_keys(source_root: Path = ROOT, *, skip_nerd: bool = False) -> dict[str, str]: """Hash each relevant input once; presentation edits are not font inputs.
The direct builder currently includes Nerd extents in the clean clipping box unless --skip-nerd is used. That mode therefore belongs to the base key, not merely to the requested-output list. """ common = [ source_root / "VERSION", source_root / "pyproject.toml", source_root / "uv.lock", source_root / "mise.toml", source_root / "features/code-forms.fea", source_root / "features/ligatures.json", source_root / "scripts/build_fonts.py", source_root / "scripts/build_cache.py", source_root / "scripts/optical_fitting.py", *sorted((source_root / "scripts").glob("font_*.py")), *sorted((source_root / "tools").rglob("*.py")), *sorted((source_root / "tools").rglob("*.toml")), *sorted((source_root / "upstream/fonts/ttf").glob("*.ttf")), *sorted((source_root / "upstream/fonts/variable").glob("*.ttf")), ] nerd_source = source_root / "vendor/nerd-fonts/SymbolsNerdFontMono-v3.4.0-NoFontLogos.ttf" if not skip_nerd: common.append(nerd_source) files = { str(path.relative_to(source_root)): sha256(path) for path in dict.fromkeys(common) } base = _digest({ "schema": SCHEMA, "inputs": files, "python": sys.version, "platform": sys.platform, "machine": platform.machine(), "epoch": os.environ.get("SOURCE_DATE_EPOCH", "0"), "nerd_metrics": not skip_nerd, }) return {kind: _digest([kind, base]) for kind in KINDS}
def stages(style: str, keys: dict[str, str]) -> tuple[Stage, ...]: return ( Stage(style, "ttf", f"ttf/IltaMono-{style}.ttf", _digest([style, keys["ttf"]])), Stage(style, "woff2", f"woff2/IltaMono-{style}.woff2", _digest([style, keys["woff2"]])), Stage(style, "nerd", f"nerd-font/IltaMonoNerdFont-{style}.ttf", _digest([style, keys["nerd"]])), )
def read_receipts(root: Path) -> dict[str, dict[str, str]]: try: data = json.loads((root / ".build-cache.json").read_text(encoding="utf-8")) except (OSError, ValueError): return {} if not isinstance(data, dict) or data.get("schema") != SCHEMA or data.get("root") != str(root): return {} records = data.get("outputs") if not isinstance(records, dict): return {} for path, record in records.items(): if not isinstance(path, str) or not isinstance(record, dict): return {} for field in ("key", "sha256"): value = record.get(field) if not isinstance(value, str) or len(value) != 64 or any(c not in "0123456789abcdef" for c in value): return {} return records
def plan( output_root: Path, keys: dict[str, str], *, force: bool = False, skip_woff2: bool = False, skip_nerd: bool = False, styles: tuple[str, ...] = tuple(style.name for style in STYLES),) -> Plan: root = output_root.resolve() receipts = read_receipts(root) required = [] pending = [] for style in styles: base_valid = False for stage in stages(style, keys): if (stage.kind == "woff2" and skip_woff2) or (stage.kind == "nerd" and skip_nerd): continue required.append(stage) receipt = receipts.get(stage.relative_path, {}) path = root / stage.relative_path valid = ( not force and receipt.get("key") == stage.key and path.is_file() and receipt.get("sha256") == sha256(path) ) if stage.kind == "ttf": base_valid = valid elif not base_valid: valid = False if not valid: pending.append(stage) return Plan(root, tuple(required), tuple(pending), receipts)
def _atomic_json(path: Path, payload: object) -> None: with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, prefix=path.name, delete=False) as handle: temporary = Path(handle.name) try: json.dump(payload, handle, sort_keys=True, indent=2) handle.write("\n") handle.flush() os.fsync(handle.fileno()) except BaseException: temporary.unlink(missing_ok=True) raise try: os.replace(temporary, path) finally: temporary.unlink(missing_ok=True)
def execute(current: Plan, *, jobs: int, skip_nerd: bool) -> bool: """Stage work privately, then publish complete outputs and their receipts.
A missing WOFF2 uses only a hash-verified base. A missing Nerd derivative rebuilds that style with the direct builder; it never blesses stale files merely because they happen to exist. Skipped receipts retain their old key. """ if not current.pending: print("[cache] verified font outputs; no-op") return False current.root.mkdir(parents=True, exist_ok=True) rebuild = {stage.style for stage in current.pending if stage.kind != "woff2"} with tempfile.TemporaryDirectory(prefix=".font-build-", dir=current.root) as temporary: work = Path(temporary) if rebuild: command = [ sys.executable, str(ROOT / "scripts/build_fonts.py"), "--output-dir", str(work), "--jobs", str(jobs), "--styles", *sorted(rebuild), ] if skip_nerd: command.append("--skip-nerd") if not any(s.kind == "woff2" and s.style in rebuild for s in current.pending): command.append("--skip-woff2") subprocess.run(command, check=True) for stage in current.pending: if stage.style in rebuild: continue # A pending WOFF2 is the only path that can avoid the direct build. from font_output import save_woff2_from_ttf base = current.root / "ttf" / f"IltaMono-{stage.style}.ttf" save_woff2_from_ttf(base, work / stage.relative_path) print(f"[cache] regenerated {stage.relative_path} from verified TTF") # Check every expected file before replacing even the first old output. produced = { stage.relative_path: {"key": stage.key, "sha256": sha256(work / stage.relative_path)} for stage in current.pending } for stage in current.pending: destination = current.root / stage.relative_path destination.parent.mkdir(parents=True, exist_ok=True) os.replace(work / stage.relative_path, destination) receipts = {**current.receipts, **produced} # Even under --force with unchanged source keys, a newly produced # base invalidates skipped derivatives. Never assume its bytes match # the previous base just because the compiler should be deterministic. for base in current.pending: if base.kind != "ttf": continue for relative in ( f"woff2/IltaMono-{base.style}.woff2", f"nerd-font/IltaMonoNerdFont-{base.style}.ttf", ): if relative not in produced: receipts.pop(relative, None) _atomic_json(current.root / ".build-cache.json", { "schema": SCHEMA, "root": str(current.root), "outputs": receipts, }) return True
@contextmanagerdef locked_output(root: Path): root.mkdir(parents=True, exist_ok=True) with (root / ".build.lock").open("a") as handle: fcntl.flock(handle, fcntl.LOCK_EX) yield
def positive_integer(value: str) -> int: number = int(value) if number < 1: raise argparse.ArgumentTypeError("must be at least 1") return number
def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output-dir", type=Path, default=ROOT / "dist/fonts") parser.add_argument("--jobs", type=positive_integer, default=2) parser.add_argument("--force", action="store_true") parser.add_argument("--skip-woff2", action="store_true") parser.add_argument("--skip-nerd", action="store_true") parser.add_argument("--stamp", type=Path, help="update this Make stamp only after changed outputs or a missing stamp") args = parser.parse_args(argv) keys = input_keys(skip_nerd=args.skip_nerd) with locked_output(args.output_dir): current = plan(args.output_dir, keys, force=args.force, skip_woff2=args.skip_woff2, skip_nerd=args.skip_nerd) changed = execute(current, jobs=args.jobs, skip_nerd=args.skip_nerd) if args.stamp is not None and (changed or not args.stamp.exists()): args.stamp.parent.mkdir(parents=True, exist_ok=True) args.stamp.touch() return 0
if __name__ == "__main__": raise SystemExit(main())