chore: Add verbose mode to build-sd-fonts.py (#1923)

## Summary

One of the recent font conversion script changes obliterated
performance. Add a verbose mode to build-sd-fonts.py to help diagnose
problems. Changed the CI process to use verbose mode.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
This commit is contained in:
Zach Nelson
2026-05-10 11:55:57 -05:00
committed by GitHub
parent aba393f900
commit 74b8cac928
2 changed files with 76 additions and 15 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT"
- name: Build SD card fonts
run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean
run: python3 lib/EpdFont/scripts/build-sd-fonts.py --clean --verbose -j 1
- name: Flatten output for release assets
run: |
+75 -14
View File
@@ -17,6 +17,12 @@ Usage:
# Generate only specific families
python3 build-sd-fonts.py --only Literata,IBMPlexMono
# Stream child process output for debugging
python3 build-sd-fonts.py --verbose
# Override the per-family timeout (default: 600s)
python3 build-sd-fonts.py --timeout 1200
"""
import argparse
@@ -25,6 +31,8 @@ import shutil
import subprocess
import sys
import tempfile
import threading
import time
import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
@@ -141,7 +149,16 @@ def resolve_font_path(style_spec: dict, family_name: str, style_name: str) -> Pa
return resolved
def build_family(family: dict, output_base: Path) -> tuple[str, bool, str]:
def _stream_pipe(pipe, prefix: str, dest: list[str]):
"""Read lines from a pipe, print with prefix, and accumulate into dest."""
for line in pipe:
dest.append(line)
print(f" [{prefix}] {line}", end="", flush=True)
def build_family(
family: dict, output_base: Path, verbose: bool = False, timeout: int = 600
) -> tuple[str, bool, str]:
"""Build a single font family. Returns (name, success, message)."""
name = family["name"]
output_dir = output_base / name
@@ -185,18 +202,52 @@ def build_family(family: dict, output_base: Path) -> tuple[str, bool, str]:
cmd.append("--force-autohint")
# Run fontconvert_sdcard.py
start = time.monotonic()
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600,
)
if result.returncode != 0:
return name, False, result.stderr.strip() or f"Exit code {result.returncode}"
return name, True, ""
except subprocess.TimeoutExpired:
return name, False, "Timed out after 600s"
if verbose:
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
stdout_lines: list[str] = []
stderr_lines: list[str] = []
t_out = threading.Thread(
target=_stream_pipe, args=(proc.stdout, name, stdout_lines)
)
t_err = threading.Thread(
target=_stream_pipe, args=(proc.stderr, f"{name}/err", stderr_lines)
)
t_out.start()
t_err.start()
try:
proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
elapsed = time.monotonic() - start
return name, False, f"Timed out after {elapsed:.0f}s"
finally:
t_out.join()
t_err.join()
if proc.returncode != 0:
err = "".join(stderr_lines).strip()
return name, False, err or f"Exit code {proc.returncode}"
return name, True, ""
else:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout,
)
if result.returncode != 0:
return name, False, result.stderr.strip() or f"Exit code {result.returncode}"
return name, True, ""
except subprocess.TimeoutExpired as e:
elapsed = time.monotonic() - start
tail = ""
captured = getattr(e, "stderr", None) or getattr(e, "stdout", None)
if captured:
lines = captured.strip().splitlines()
tail = "\n Last output:\n" + "\n".join(f" | {l}" for l in lines[-20:])
return name, False, f"Timed out after {elapsed:.0f}s{tail}"
except Exception as e:
return name, False, str(e)
@@ -252,6 +303,14 @@ def main():
help="Max parallel jobs (default: number of families)"
)
parser.add_argument("--clean", action="store_true", help="Clean output directory before building")
parser.add_argument(
"--verbose", "-v", action="store_true",
help="Stream child process output in real time (useful for debugging timeouts)"
)
parser.add_argument(
"--timeout", type=int, default=600,
help="Per-family timeout in seconds (default: 600)"
)
args = parser.parse_args()
if args.manifest and not args.base_url:
@@ -303,12 +362,14 @@ def main():
# Build phase (parallel)
max_workers = args.jobs or len(families)
print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs) ===\n")
verbose = args.verbose
timeout = args.timeout
print(f"\n=== Building {len(families)} families ({max_workers} parallel jobs, timeout {timeout}s) ===\n")
failed = []
with ProcessPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(build_family, family, output_base): family["name"]
executor.submit(build_family, family, output_base, verbose, timeout): family["name"]
for family in families
}
for future in as_completed(futures):