fix(producer): degrade gracefully when font cache directory is unwritable (#3572)

* fix(producer): degrade gracefully when font cache directory is unwritable

When the font cache root (~/.cache/hyperframes/fonts/) cannot be created
(EPERM on read-only filesystems, restricted home directories, etc.), the
render aborts with a raw mkdir error. The cache is an optimization, not
a requirement — a missing cache should mean slower first renders, not
broken renders.

Fall back to a temporary directory under os.tmpdir() when the configured
cache root fails, so Google Fonts downloads still proceed. The fallback
cache is per-process and not persistent across renders, but the render
completes.

Fixes #3412.

* fix: use mkdtempSync for font cache fallback, restore warning

Rames Jusso's review caught a regression in the force-push: the
predictable tmpdir path is unsafe (symlink attacks in world-writable
dirs), and the warning log was dropped. Restore the mkdtempSync
pattern matching lambdaFontCacheRoot, add a CLI hint per Miguel's
request, and reuse the ephemeral root across calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: emit font cache fallback warning once per run, not per font

Gate the warning on whether this is the first fallback activation.
The ??= already suppresses repeat mkdtempSync, but the warn fired
for every font family in the composition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
miga-heygen
2026-09-03 04:00:37 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 5a5e841c5d
commit da09428af1
@@ -746,10 +746,27 @@ function fontSlug(familyName: string): string {
.replace(/^-|-$/g, "");
}
let ephemeralFontCacheRoot: string | undefined;
function fontCacheDir(slug: string): string {
const dir = join(resolveFontCacheRoot(), slug);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
try {
mkdirSync(dir, { recursive: true });
} catch {
const firstFallback = ephemeralFontCacheRoot === undefined;
ephemeralFontCacheRoot ??= mkdtempSync(join(tmpdir(), "hyperframes-fonts-"));
const fallback = join(ephemeralFontCacheRoot, slug);
mkdirSync(fallback, { recursive: true });
if (firstFallback) {
defaultLogger.warn(
`Font cache directory is unwritable (${dir}). ` +
`Using temporary fallback — fonts will re-download each run. ` +
`Fix with: chmod 755 ${resolveFontCacheRoot()}`,
);
}
return fallback;
}
}
return dir;
}