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" echo "metadata=$(python3 -c 'from cpfont_version import FONTS_MANIFEST_VERSION; print(FONTS_MANIFEST_VERSION)')" >> "$GITHUB_OUTPUT"
- name: Build SD card fonts - 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 - name: Flatten output for release assets
run: | run: |
+75 -14
View File
@@ -17,6 +17,12 @@ Usage:
# Generate only specific families # Generate only specific families
python3 build-sd-fonts.py --only Literata,IBMPlexMono 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 import argparse
@@ -25,6 +31,8 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import threading
import time
import urllib.request import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
@@ -141,7 +149,16 @@ def resolve_font_path(style_spec: dict, family_name: str, style_name: str) -> Pa
return resolved 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).""" """Build a single font family. Returns (name, success, message)."""
name = family["name"] name = family["name"]
output_dir = output_base / 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") cmd.append("--force-autohint")
# Run fontconvert_sdcard.py # Run fontconvert_sdcard.py
start = time.monotonic()
try: try:
result = subprocess.run( if verbose:
cmd, proc = subprocess.Popen(
capture_output=True, cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
text=True, )
timeout=600, stdout_lines: list[str] = []
) stderr_lines: list[str] = []
if result.returncode != 0: t_out = threading.Thread(
return name, False, result.stderr.strip() or f"Exit code {result.returncode}" target=_stream_pipe, args=(proc.stdout, name, stdout_lines)
return name, True, "" )
except subprocess.TimeoutExpired: t_err = threading.Thread(
return name, False, "Timed out after 600s" 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: except Exception as e:
return name, False, str(e) return name, False, str(e)
@@ -252,6 +303,14 @@ def main():
help="Max parallel jobs (default: number of families)" help="Max parallel jobs (default: number of families)"
) )
parser.add_argument("--clean", action="store_true", help="Clean output directory before building") 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() args = parser.parse_args()
if args.manifest and not args.base_url: if args.manifest and not args.base_url:
@@ -303,12 +362,14 @@ def main():
# Build phase (parallel) # Build phase (parallel)
max_workers = args.jobs or len(families) 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 = [] failed = []
with ProcessPoolExecutor(max_workers=max_workers) as executor: with ProcessPoolExecutor(max_workers=max_workers) as executor:
futures = { 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 family in families
} }
for future in as_completed(futures): for future in as_completed(futures):