Problem
sphinx_module's PlantUML rendering crashes on CI (RBE with --remote_download_minimal) with:
RuntimeException: Fontconfig head is null, check your fonts or fonts configuration
thrown by sun.awt.FontConfiguration.readFontConfigFile(). Root cause: the hermetic JDK toolchain (remotejdk21_linux) ships no $JAVA_HOME/conf/fonts/fontconfig.properties, so it always falls into a native auto-generation path that crashes in the fontless build sandbox. This is unrelated to Linux fonts.conf/FONTCONFIG_FILE — confirmed setting that env var has zero effect on this JDK-internal mechanism.
A working fix was prototyped downstream in conf.template.py via the documented -Dsun.awt.fontconfig=<path> JVM flag, pointed at a minimal properties file referencing a DejaVu font from the same sysroot dot already uses via exec_in_sysroot/fakechroot. That prototype confirmed the approach resolves the crash and restores SVG rendering, but its implementation has known soundness gaps described below.
Why the prototype is a workaround, not a sound fix
- Sysroot path discovery is glob-based, not a real dependency edge. The prototype searches
graphviz_dot + ".runfiles" recursively for a directory literally named dot_sysroot. This only works because of a naming convention in exec_in_sysroot.bzl ("<name>_sysroot"), not because the Sphinx build target actually declares a dependency on the sysroot. Any rename/refactor of exec_in_sysroot silently breaks font resolution.
- Root architectural gap: the Sphinx doc action depends on
dot (the launcher) but not on dot_sysroot (the TreeArtifact) directly. Under --remote_download_minimal, dot_sysroot is therefore not staged as a bazel-out sibling and is only reachable via dot's own runfiles — an implementation detail of exec_in_sysroot's launcher, not a contract.
- Non-hermetic runtime side effect: the prototype calls
tempfile.mkdtemp() and writes fontconfig.properties at Sphinx-build Python-execution time, outside Bazel's action graph. Not cached, not sandboxed, not reproducible as a build artifact.
- Hardcoded single font: assumes
DejaVuSans.ttf exists at a fixed path inside the sysroot and maps it to a made-up subset name (alphabetic) rather than the standard JDK logical-font mapping (serif/sansserif/monospaced/dialog/dialoginput). Silently breaks if the graphviz apt package's font deps change.
Suggested fix
- Add an explicit provider/attribute from
exec_in_sysroot (or the dot-producing rule) exposing the sysroot's File/TreeArtifact directly to consumers — e.g. a SysrootInfo provider with a root field — instead of relying on runfiles-directory-naming conventions.
- Wire the Sphinx/PlantUML build rule to declare an explicit
data/dep on that sysroot (same pattern already used for other build-time env vars passed to conf.py), so it's staged correctly under --remote_download_minimal without glob search.
- Generate
fontconfig.properties as a genrule/build action (tracked Bazel output), not at Python runtime in conf.py. Pass its path in via an env var set by the rule.
- Use the standard JDK logical-font mapping (
serif, sansserif, monospaced, dialog, dialoginput → real font files) instead of a single custom alphabetic subset, so it degrades more gracefully if font files change.
References
Problem
sphinx_module's PlantUML rendering crashes on CI (RBE with--remote_download_minimal) with:thrown by
sun.awt.FontConfiguration.readFontConfigFile(). Root cause: the hermetic JDK toolchain (remotejdk21_linux) ships no$JAVA_HOME/conf/fonts/fontconfig.properties, so it always falls into a native auto-generation path that crashes in the fontless build sandbox. This is unrelated to Linuxfonts.conf/FONTCONFIG_FILE— confirmed setting that env var has zero effect on this JDK-internal mechanism.A working fix was prototyped downstream in
conf.template.pyvia the documented-Dsun.awt.fontconfig=<path>JVM flag, pointed at a minimal properties file referencing a DejaVu font from the same sysrootdotalready uses viaexec_in_sysroot/fakechroot. That prototype confirmed the approach resolves the crash and restores SVG rendering, but its implementation has known soundness gaps described below.Why the prototype is a workaround, not a sound fix
graphviz_dot + ".runfiles"recursively for a directory literally nameddot_sysroot. This only works because of a naming convention inexec_in_sysroot.bzl("<name>_sysroot"), not because the Sphinx build target actually declares a dependency on the sysroot. Any rename/refactor ofexec_in_sysrootsilently breaks font resolution.dot(the launcher) but not ondot_sysroot(the TreeArtifact) directly. Under--remote_download_minimal,dot_sysrootis therefore not staged as a bazel-out sibling and is only reachable viadot's own runfiles — an implementation detail ofexec_in_sysroot's launcher, not a contract.tempfile.mkdtemp()and writesfontconfig.propertiesat Sphinx-build Python-execution time, outside Bazel's action graph. Not cached, not sandboxed, not reproducible as a build artifact.DejaVuSans.ttfexists at a fixed path inside the sysroot and maps it to a made-up subset name (alphabetic) rather than the standard JDK logical-font mapping (serif/sansserif/monospaced/dialog/dialoginput). Silently breaks if thegraphvizapt package's font deps change.Suggested fix
exec_in_sysroot(or thedot-producing rule) exposing the sysroot'sFile/TreeArtifactdirectly to consumers — e.g. aSysrootInfoprovider with arootfield — instead of relying on runfiles-directory-naming conventions.data/dep on that sysroot (same pattern already used for other build-time env vars passed toconf.py), so it's staged correctly under--remote_download_minimalwithout glob search.fontconfig.propertiesas a genrule/build action (tracked Bazel output), not at Python runtime inconf.py. Pass its path in via an env var set by the rule.serif,sansserif,monospaced,dialog,dialoginput→ real font files) instead of a single customalphabeticsubset, so it degrades more gracefully if font files change.References