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:
@@ -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: |
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user