From 1baebd2d098ce1d81b9e8ddd4de0fca13b8ff9bd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 14:08:17 +0000 Subject: [PATCH] fix(fonts): resolve asset paths against the install root, not the cwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FontManager built its catalog from cwd-relative paths ('assets/fonts'), so any process started outside the install root — the plugin safety harness on CI being the recurring case — found no fonts and silently degraded every plugin to PIL's default face. Several plugins grew per-plugin workarounds for exactly this (countdown, text-display, tide-display in the plugins monorepo). Catalog population now falls back to the install root derived from this module's location when the cwd-relative path is missing; behavior when running from the install root is unchanged. Verified: resolve_font returns the real FreeType face from a foreign cwd, and the full unit suites (266 tests) pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 --- src/font_manager.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/font_manager.py b/src/font_manager.py index 08a3f31e..8be283be 100644 --- a/src/font_manager.py +++ b/src/font_manager.py @@ -659,6 +659,25 @@ class FontManager: # ==================== Font Discovery ==================== + @staticmethod + def _resolve_asset_path(relative_path: str) -> str: + """Resolve a repo-relative asset path independently of the process cwd. + + Prefers the working directory (preserving behavior when the process + runs from the install root), then falls back to the install root + derived from this module's own location. Without the fallback, any + process started outside the install root (e.g. the plugin safety + harness on CI) silently loses every font and degrades to PIL's + default face. + """ + if os.path.exists(relative_path): + return relative_path + install_root = Path(__file__).resolve().parent.parent + candidate = install_root / relative_path + if candidate.exists(): + return str(candidate) + return relative_path + def _initialize_fonts(self): """Initialize font catalog and validate configuration.""" self._scan_fonts_directory() @@ -667,7 +686,7 @@ class FontManager: def _scan_fonts_directory(self): """Scan assets/fonts directory for available fonts.""" - fonts_dir = "assets/fonts" + fonts_dir = self._resolve_asset_path("assets/fonts") if not os.path.exists(fonts_dir): logger.warning(f"Fonts directory not found: {fonts_dir}") return @@ -683,6 +702,7 @@ class FontManager: def _register_common_fonts(self): """Register common font aliases from common_fonts dictionary.""" for family_name, font_path in self.common_fonts.items(): + font_path = self._resolve_asset_path(font_path) # Check if font file exists if os.path.exists(font_path): # Register the common font name (overrides auto-generated name if exists)