Convert from Homebrew Formulae to Casks for font installation

Formulae can't install to ~/Library/Fonts/ due to Homebrew sandboxing.
Casks have a built-in `font` artifact that handles this automatically.

- Replace Formula/ with Casks/ directory
- Rewrite generator to produce cask files instead of formulae
- Add .ttc (TrueType Collection) support
- Update all tests for cask format
- Update CLI and documentation
- Fonts with no installable files (TTF/OTF/TTC) are skipped

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Troutman 2026-03-07 21:38:59 -06:00
parent cb1918c30f
commit 76743cdc4d
No known key found for this signature in database
273 changed files with 2509 additions and 10048 deletions

View file

@ -3,10 +3,10 @@ from pathlib import Path
import pytest
# Repo root: directory containing font_files/ and Formula/
# Repo root: directory containing font_files/ and Casks/
REPO_ROOT = Path(__file__).resolve().parent.parent
FONT_FILES_DIR = REPO_ROOT / "font_files"
FORMULA_DIR = REPO_ROOT / "Formula"
CASKS_DIR = REPO_ROOT / "Casks"
REQUIRED_SUBDIRS = ("ttf", "otf", "web", "other_files")
WEB_EXTENSIONS = (".woff", ".woff2", ".eot", ".svg")
@ -35,8 +35,8 @@ def font_dir_names():
@pytest.fixture(scope="session")
def formula_paths():
"""All Formula/font-*.rb paths that exist."""
if not FORMULA_DIR.exists():
def cask_paths():
"""All Casks/font-*.rb paths that exist."""
if not CASKS_DIR.exists():
return []
return sorted(FORMULA_DIR.glob("font-*.rb"))
return sorted(CASKS_DIR.glob("font-*.rb"))

View file

@ -1,42 +1,61 @@
"""Tests for formula content: correct paths and valid Ruby class name."""
"""Tests for cask content: correct font paths and valid cask identifier."""
import re
import pytest
from tests.conftest import FONT_FILES_DIR, FORMULA_DIR, get_font_dir_names
from tests.conftest import CASKS_DIR, FONT_FILES_DIR, get_font_dir_names
def formula_name_to_class(formula_name: str) -> str:
"""Same logic as generator: formula name to PascalCase (no hyphens)."""
parts = re.split(r"[-_]+", formula_name)
return "".join(p.capitalize() for p in parts if p)
def _skip_if_no_cask(font_name):
cask_path = CASKS_DIR / f"{font_name}.rb"
if not cask_path.exists():
pytest.skip(f"No cask for {font_name} (no installable font files)")
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_formula_references_correct_font_path(font_name):
"""Generated formula contains the correct font_files/font-<name>/ path."""
formula_path = FORMULA_DIR / f"{font_name}.rb"
content = formula_path.read_text()
# Install block should reference this font path
def test_cask_references_correct_font_path(font_name):
"""Generated cask contains the correct font_files/font-<name>/ path."""
_skip_if_no_cask(font_name)
cask_path = CASKS_DIR / f"{font_name}.rb"
content = cask_path.read_text()
assert f"font_files/{font_name}/" in content, (
f"{font_name}: formula does not reference font_files/{font_name}/"
f"{font_name}: cask does not reference font_files/{font_name}/"
)
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_formula_class_name_valid_and_matches(font_name):
"""Formula defines class Font<PascalCase> with no hyphens (valid Ruby)."""
formula_path = FORMULA_DIR / f"{font_name}.rb"
content = formula_path.read_text()
formula_name = font_name.replace("font-", "", 1)
expected_class = "Font" + formula_name_to_class(formula_name)
# Class line: class FontSomething < Formula
match = re.search(r"class\s+(Font\w+)\s+<\s+Formula", content)
assert match, f"{font_name}: no 'class Font... < Formula' found"
actual_class = match.group(1)
assert "-" not in actual_class, (
f"{font_name}: Ruby class name must not contain hyphens (got {actual_class})"
)
assert actual_class == expected_class, (
f"{font_name}: expected class {expected_class}, got {actual_class}"
def test_cask_identifier_matches(font_name):
"""Cask declares the correct identifier matching the file name."""
_skip_if_no_cask(font_name)
cask_path = CASKS_DIR / f"{font_name}.rb"
content = cask_path.read_text()
match = re.search(r'cask\s+"([^"]+)"\s+do', content)
assert match, f"{font_name}: no 'cask \"...\" do' found"
assert match.group(1) == font_name, (
f"{font_name}: expected cask \"{font_name}\", got \"{match.group(1)}\""
)
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_cask_has_font_artifacts(font_name):
"""Cask declares at least one font artifact."""
_skip_if_no_cask(font_name)
cask_path = CASKS_DIR / f"{font_name}.rb"
content = cask_path.read_text()
font_lines = re.findall(r'^\s*font\s+"[^"]+"', content, re.MULTILINE)
assert font_lines, f"{font_name}: cask has no font artifacts"
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_cask_font_files_exist_on_disk(font_name):
"""Every font artifact declared in the cask corresponds to a real file in font_files/."""
_skip_if_no_cask(font_name)
cask_path = CASKS_DIR / f"{font_name}.rb"
content = cask_path.read_text()
# Extract paths from font "..." lines
paths = re.findall(r'^\s*font\s+"([^"]+)"', content, re.MULTILINE)
for rel_path in paths:
full_path = FONT_FILES_DIR.parent / rel_path
assert full_path.is_file(), (
f"{font_name}: font artifact references missing file: {rel_path}"
)

View file

@ -1,14 +1,26 @@
"""Tests that a Formula file exists for every font."""
"""Tests that a Cask file exists for every font with installable files."""
import pytest
from tests.conftest import FORMULA_DIR, get_font_dir_names
from tests.conftest import CASKS_DIR, FONT_FILES_DIR, get_font_dir_names
INSTALLABLE_EXTS = (".ttf", ".otf", ".ttc")
def _has_installable_fonts(font_name: str) -> bool:
font_dir = FONT_FILES_DIR / font_name
for ext in INSTALLABLE_EXTS:
pattern = f"*{ext}"
if any((font_dir / "ttf").glob(pattern)) or any((font_dir / "otf").glob(pattern)):
return True
return False
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_formula_file_exists(font_name):
"""Formula/font-<name>.rb exists for each font folder."""
# font_name is e.g. "font-acrylic-hand"; formula file is font-acrylic-hand.rb
formula_path = FORMULA_DIR / f"{font_name}.rb"
assert formula_path.is_file(), (
f"Missing formula for {font_name}: expected {formula_path}"
def test_cask_file_exists(font_name):
"""Casks/font-<name>.rb exists for each font folder with installable fonts."""
if not _has_installable_fonts(font_name):
pytest.skip(f"{font_name} has no installable font files (TTF/OTF/TTC)")
cask_path = CASKS_DIR / f"{font_name}.rb"
assert cask_path.is_file(), (
f"Missing cask for {font_name}: expected {cask_path}"
)

View file

@ -1,105 +1,25 @@
"""Verify that each font's formula install logic would copy the right files (simulated install)."""
import shutil
import tempfile
from pathlib import Path
"""Verify that each font's cask references valid font files that exist."""
import re
import pytest
from tests.conftest import (
FONT_FILES_DIR,
FORMULA_DIR,
WEB_EXTENSIONS,
get_font_dir_names,
)
def _run_install_simulation(font_name: str, font_dir: Path, prefix: Path, formula_name: str) -> None:
"""
Simulate the formula's install: copy ttf/otf/web/other_files to prefix
as the formula would (same layout as homebrew-fonts/font_files/<font_name>/).
"""
# Layout formula expects: homebrew-fonts/font_files/<font_name>/{ttf,otf,web,other_files}
# We use font_dir directly (same layout).
(prefix / "fonts").mkdir(parents=True)
(prefix / "fonts/truetype").mkdir(parents=True)
(prefix / "fonts/opentype").mkdir(parents=True)
(prefix / "fonts/webfonts").mkdir(parents=True)
(prefix / formula_name).mkdir(parents=True)
for f in (font_dir / "ttf").glob("*.ttf"):
shutil.copy2(f, prefix / "fonts/truetype" / f.name)
for f in (font_dir / "otf").glob("*.otf"):
shutil.copy2(f, prefix / "fonts/opentype" / f.name)
web_dir = font_dir / "web"
for f in web_dir.iterdir():
if f.is_file() and f.suffix.lower() in WEB_EXTENSIONS:
shutil.copy2(f, prefix / "fonts/webfonts" / f.name)
other_src = font_dir / "other_files"
if other_src.exists():
for f in other_src.iterdir():
dest = prefix / formula_name / f.name
if f.is_dir():
shutil.copytree(f, dest, dirs_exist_ok=True)
else:
shutil.copy2(f, dest)
from tests.conftest import CASKS_DIR, FONT_FILES_DIR, get_font_dir_names
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_install_simulation_places_files(font_name):
"""Simulate install for this font and assert expected files are present under prefix."""
font_dir = FONT_FILES_DIR / font_name
formula_name = font_name.replace("font-", "", 1)
def test_cask_font_artifacts_are_installable(font_name):
"""Each font artifact in the cask points to a real TTF or OTF file."""
cask_path = CASKS_DIR / f"{font_name}.rb"
if not cask_path.exists():
pytest.skip(f"No cask for {font_name} (no TTF/OTF files)")
with tempfile.TemporaryDirectory() as tmp:
prefix = Path(tmp) / "prefix"
prefix.mkdir()
_run_install_simulation(font_name, font_dir, prefix, formula_name)
content = cask_path.read_text()
paths = re.findall(r'^\s*font\s+"([^"]+)"', content, re.MULTILINE)
assert paths, f"{font_name}: cask has no font artifacts"
# Same assertions as the formula's test block: at least one expected path has content
ttf_files = list((prefix / "fonts/truetype").glob("*.ttf"))
otf_files = list((prefix / "fonts/opentype").glob("*.otf"))
web_files = list((prefix / "fonts/webfonts").iterdir()) if (prefix / "fonts/webfonts").exists() else []
other_dir = prefix / formula_name
has_other = other_dir.exists() and any(other_dir.iterdir())
has_ttf = len(ttf_files) > 0
has_otf = len(otf_files) > 0
has_web = len(web_files) > 0
assert has_ttf or has_otf or has_web or has_other, (
f"{font_name}: after simulated install, no files in share/fonts/truetype, "
f"share/fonts/opentype, share/fonts/webfonts, or share/{formula_name}"
)
@pytest.mark.parametrize("font_name", get_font_dir_names())
def test_install_simulation_file_counts_match_source(font_name):
"""After simulated install, number of installed font files matches font_files/."""
font_dir = FONT_FILES_DIR / font_name
formula_name = font_name.replace("font-", "", 1)
with tempfile.TemporaryDirectory() as tmp:
prefix = Path(tmp) / "prefix"
prefix.mkdir()
_run_install_simulation(font_name, font_dir, prefix, formula_name)
ttf_src = len(list((font_dir / "ttf").glob("*.ttf")))
otf_src = len(list((font_dir / "otf").glob("*.otf")))
web_src = sum(
1 for f in (font_dir / "web").iterdir()
if f.is_file() and f.suffix.lower() in WEB_EXTENSIONS
)
ttf_installed = len(list((prefix / "fonts/truetype").glob("*.ttf")))
otf_installed = len(list((prefix / "fonts/opentype").glob("*.otf")))
web_installed = len(list((prefix / "fonts/webfonts").iterdir()))
assert ttf_src == ttf_installed, (
f"{font_name}: TTF count mismatch: source={ttf_src}, installed={ttf_installed}"
)
assert otf_src == otf_installed, (
f"{font_name}: OTF count mismatch: source={otf_src}, installed={otf_installed}"
)
assert web_src == web_installed, (
f"{font_name}: web font count mismatch: source={web_src}, installed={web_installed}"
for rel_path in paths:
full_path = FONT_FILES_DIR.parent / rel_path
assert full_path.is_file(), f"{font_name}: missing font file: {rel_path}"
assert full_path.suffix.lower() in (".ttf", ".otf", ".ttc"), (
f"{font_name}: unexpected font type: {full_path.suffix}"
)

View file

@ -1,34 +1,40 @@
"""Global tests: no orphan formulae, no duplicate formula names."""
import pytest
from tests.conftest import FONT_FILES_DIR, FORMULA_DIR, get_font_dir_names
"""Global tests: no orphan casks, no duplicate cask names."""
from tests.conftest import CASKS_DIR, FONT_FILES_DIR, get_font_dir_names
def test_every_formula_has_matching_font_folder():
"""Every Formula/font-*.rb has a matching font_files/font-<name>/ directory."""
formula_files = sorted(FORMULA_DIR.glob("font-*.rb"))
for formula_path in formula_files:
name = formula_path.stem # e.g. font-acrylic-hand
def test_every_cask_has_matching_font_folder():
"""Every Casks/font-*.rb has a matching font_files/font-<name>/ directory."""
cask_files = sorted(CASKS_DIR.glob("font-*.rb"))
for cask_path in cask_files:
name = cask_path.stem # e.g. font-acrylic-hand
font_dir = FONT_FILES_DIR / name
assert font_dir.is_dir(), (
f"Orphan formula: {formula_path.name} has no matching font folder {font_dir}"
f"Orphan cask: {cask_path.name} has no matching font folder {font_dir}"
)
def test_no_duplicate_formula_names():
"""Only one formula file per font-<name> (no duplicate basenames)."""
formula_files = list(FORMULA_DIR.glob("font-*.rb"))
names = [p.stem for p in formula_files]
def test_no_duplicate_cask_names():
"""Only one cask file per font-<name> (no duplicate basenames)."""
cask_files = list(CASKS_DIR.glob("font-*.rb"))
names = [p.stem for p in cask_files]
seen = set()
for n in names:
assert n not in seen, f"Duplicate formula name: {n}.rb"
assert n not in seen, f"Duplicate cask name: {n}.rb"
seen.add(n)
def test_formula_count_matches_font_count():
"""Number of font-*.rb formulae equals number of font-* directories."""
def test_cask_count_matches_font_count():
"""Number of font-*.rb casks equals number of font-* directories with TTF/OTF files."""
font_names = get_font_dir_names()
formula_count = len(list(FORMULA_DIR.glob("font-*.rb")))
assert formula_count == len(font_names), (
f"Formula count ({formula_count}) != font dir count ({len(font_names)})"
# Only count fonts that have TTF or OTF files (casks skip fonts with no installable files)
fonts_with_files = []
for name in font_names:
font_dir = FONT_FILES_DIR / name
has_ttf = any((font_dir / "ttf").glob("*.ttf")) or any((font_dir / "ttf").glob("*.ttc"))
has_otf = any((font_dir / "otf").glob("*.otf"))
if has_ttf or has_otf:
fonts_with_files.append(name)
cask_count = len(list(CASKS_DIR.glob("font-*.rb")))
assert cask_count == len(fonts_with_files), (
f"Cask count ({cask_count}) != font dir count with TTF/OTF ({len(fonts_with_files)})"
)