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

@ -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}"
)