Files

128 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""
Scan a Kotlin project for `MaterialSymbols.<Name>` references and emit a
usage-codepoint.txt that lists every name + codepoint actually used.
The file is what the Gradle `subsetMaterialSymbols<Style>` task reads to
decide which glyphs to keep in the bundled font. Splitting discovery
(this script, runs on the build machine's Python) from subsetting (the
Gradle task, runs hb-subset) lets the same usage file feed all three
style modules and lets you eyeball / diff it in CI without touching
fonts.
Output format - UTF-8 text, one entry per line:
# Generated by scripts/subset-material-symbols.py
# 142 icons referenced, 140 resolved, 2 unknown
Add=0xe145
ArrowBack=0xe5c4
...
# Unknown (not in MaterialSymbols.kt):
# SomeMissingName
Usage:
python3 scripts/subset-material-symbols.py \\
--src apidemo/src demo/src \\
--out build/icons/usage-codepoint.txt
Defaults to scanning the whole repo and writing to
build/icons/usage-codepoint.txt at the repo root.
"""
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
# Pulls "Lock", "0xe897" out of `const val Lock = 0xe897`.
CONST_RE = re.compile(r"const\s+val\s+(\w+)\s*=\s*0x([0-9a-fA-F]+)")
# Matches MaterialSymbols.Name in any Kotlin source.
USAGE_RE = re.compile(r"\bMaterialSymbols\.(\w+)\b")
DEFAULT_CONSTANTS = REPO_ROOT / "utils/material-symbols/src/commonMain/kotlin/com/compose/sdl/icons/MaterialSymbols.kt"
DEFAULT_OUTPUT = REPO_ROOT / "build/icons/usage-codepoint.txt"
def parse_constants(path: Path) -> dict[str, int]:
"""Read MaterialSymbols.kt → {Name: codepoint}."""
icons: dict[str, int] = {}
with path.open(encoding="utf-8") as f:
for line in f:
m = CONST_RE.search(line)
if m:
icons[m.group(1)] = int(m.group(2), 16)
return icons
def scan_usages(src_roots: list[Path]) -> set[str]:
"""Walk each src root, grep every .kt file for MaterialSymbols.<Name>."""
used: set[str] = set()
for root in src_roots:
if not root.exists():
print(f"warn: source root {root} does not exist", file=sys.stderr)
continue
for path in root.rglob("*.kt"):
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
for m in USAGE_RE.finditer(text):
used.add(m.group(1))
return used
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--src", nargs="+", default=[],
help="Source root(s) to scan. Defaults to the entire repo.")
ap.add_argument("--constants", type=Path, default=DEFAULT_CONSTANTS,
help="Path to MaterialSymbols.kt (icon → codepoint map).")
ap.add_argument("--extra", default="",
help="Comma-separated extra icon NAMES to keep even if not detected in sources.")
ap.add_argument("--out", type=Path, default=DEFAULT_OUTPUT,
help="Output file (default: build/icons/usage-codepoint.txt).")
args = ap.parse_args()
src_roots = [Path(s).resolve() for s in args.src] or [REPO_ROOT]
constants = parse_constants(args.constants)
if not constants:
print(f"error: no codepoints parsed from {args.constants}", file=sys.stderr)
return 2
used_names = scan_usages(src_roots)
used_names.update(n.strip() for n in args.extra.split(",") if n.strip())
resolved: list[tuple[str, int]] = []
missing: list[str] = []
for name in sorted(used_names):
cp = constants.get(name)
if cp is None:
missing.append(name)
else:
resolved.append((name, cp))
args.out.parent.mkdir(parents=True, exist_ok=True)
with args.out.open("w", encoding="utf-8") as f:
f.write("# Generated by scripts/subset-material-symbols.py - do not edit.\n")
f.write(f"# {len(used_names)} icons referenced, {len(resolved)} resolved, {len(missing)} unknown\n")
f.write(f"# Scanned: {', '.join(str(r) for r in src_roots)}\n")
for name, cp in resolved:
f.write(f"{name}=0x{cp:04x}\n")
if missing:
f.write("# Unknown (not in MaterialSymbols.kt):\n")
for name in missing:
f.write(f"# {name}\n")
print(f"Wrote {args.out}")
print(f" {len(used_names)} referenced · {len(resolved)} resolved · {len(missing)} unknown")
if missing:
print(f" Unknown names: {', '.join(missing)}")
return 0
if __name__ == "__main__":
sys.exit(main())