|
20 | 20 | theme, etc.): |
21 | 21 |
|
22 | 22 | - Hermetic PlantUML / Graphviz / FTA-metamodel resolution. The env vars this |
23 | | - reads (PLANTUML_BIN, GRAPHVIZ_DOT, FTA_METAMODEL_DIR) are set |
24 | | - unconditionally by `_hermetic_tool_env()` in sphinx_module.bzl for every |
25 | | - SphinxNeedsBuild/SphinxHtmlBuild action, regardless of which toolchain or |
26 | | - conf_template is in effect - so this works for any consumer without extra |
27 | | - Bazel wiring. See docs/tooling_architecture.rst |
28 | | - §"Hermetic tool path resolution". |
| 23 | + reads (PLANTUML_BIN, GRAPHVIZ_DOT, FTA_METAMODEL_DIR, |
| 24 | + PLANTUML_FONTCONFIG_DIR) are set unconditionally by `_hermetic_tool_env()` |
| 25 | + in sphinx_module.bzl for every SphinxNeedsBuild/SphinxHtmlBuild action, |
| 26 | + regardless of which toolchain or conf_template is in effect - so this |
| 27 | + works for any consumer without extra Bazel wiring. See |
| 28 | + docs/tooling_architecture.rst §"Hermetic tool path resolution". |
29 | 29 | - sphinx-needs external-needs loading, re-exported from bazel_sphinx_needs |
30 | 30 | rather than re-derived (see that module's docstring for the JSON format). |
31 | 31 | - The sphinx-needs type/option/link schema loaded from the upstream S-CORE |
|
36 | 36 |
|
37 | 37 | import logging |
38 | 38 | import os |
| 39 | +import tempfile |
39 | 40 | from typing import Any, Dict, List, Optional |
40 | 41 |
|
41 | 42 | from bazel_sphinx_needs import ( |
@@ -154,14 +155,80 @@ def resolve_fta_metamodel_dir() -> str: |
154 | 155 | return resolved |
155 | 156 |
|
156 | 157 |
|
| 158 | +def resolve_plantuml_fontconfig() -> Optional[str]: |
| 159 | + """Resolve a ready-to-use sun.awt.FontConfiguration properties file from |
| 160 | + PLANTUML_FONTCONFIG_DIR, or None (with a warning) if it can't be built. |
| 161 | +
|
| 162 | + OpenJDK on Linux normally builds its logical-font (Serif, SansSerif, ...) |
| 163 | + mapping by querying the native libfontconfig library and the host's |
| 164 | + installed fonts. In a minimal container/toolchain with neither, that |
| 165 | + query fails, and -- because there's also no fontconfig.properties bundled |
| 166 | + with the JDK to fall back to -- PlantUML crashes the first time it asks |
| 167 | + for any font metric with "Fontconfig head is null, check your fonts or |
| 168 | + fonts configuration" (surfaced early by Run.forceOpenJdkResourceLoad). |
| 169 | + -Djava.awt.headless=true does not avoid this; the failing font-manager |
| 170 | + init happens regardless of headless mode. |
| 171 | +
|
| 172 | + PLANTUML_FONTCONFIG_DIR (set by sphinx_module.bzl's _hermetic_tool_env()) |
| 173 | + points at a directory containing `fontconfig.properties.tpl` (a |
| 174 | + sun.awt.FontConfiguration properties template with a `{font_path}` |
| 175 | + placeholder) and the bundled `LiberationSans-Regular.ttf` fallback font -- see |
| 176 | + //third_party/plantuml:fontconfig_fallback. This substitutes the font's |
| 177 | + resolved absolute path into the template and writes the result to a |
| 178 | + fresh temp file, since the template and font, while always siblings on |
| 179 | + disk, can't reference each other by a fixed relative path: the JVM |
| 180 | + resolves a properties file's `filename.*` values against its own current |
| 181 | + working directory, not the properties file's location, and that |
| 182 | + directory varies with the Bazel sandbox/runfiles layout of whichever |
| 183 | + action executes PlantUML. |
| 184 | +
|
| 185 | + Returns: |
| 186 | + Absolute path to the generated properties file, or None if |
| 187 | + PLANTUML_FONTCONFIG_DIR is unset or fontconfig.properties.tpl is |
| 188 | + missing from it (a warning is logged either way). |
| 189 | + """ |
| 190 | + raw = os.environ.get("PLANTUML_FONTCONFIG_DIR", "") |
| 191 | + if not raw: |
| 192 | + logger.warning( |
| 193 | + "PLANTUML_FONTCONFIG_DIR is not set; PlantUML may crash with " |
| 194 | + '"Fontconfig head is null" in environments without a native ' |
| 195 | + "fontconfig library and fonts installed." |
| 196 | + ) |
| 197 | + return None |
| 198 | + |
| 199 | + fontconfig_dir = os.path.abspath(raw) |
| 200 | + template_path = os.path.join(fontconfig_dir, "fontconfig.properties.tpl") |
| 201 | + font_path = os.path.join(fontconfig_dir, "LiberationSans-Regular.ttf") |
| 202 | + try: |
| 203 | + with open(template_path, "r", encoding="utf-8") as f: |
| 204 | + template = f.read() |
| 205 | + except OSError as e: |
| 206 | + logger.warning("Failed to read PlantUML fontconfig template %s: %s", template_path, e) |
| 207 | + return None |
| 208 | + |
| 209 | + resolved = template.replace("{font_path}", font_path) |
| 210 | + with tempfile.NamedTemporaryFile( |
| 211 | + mode="w", |
| 212 | + suffix=".properties", |
| 213 | + prefix="plantuml-fontconfig-", |
| 214 | + delete=False, |
| 215 | + encoding="utf-8", |
| 216 | + ) as out: |
| 217 | + out.write(resolved) |
| 218 | + properties_path = out.name |
| 219 | + |
| 220 | + logger.debug("plantuml fontconfig fallback resolved: %s (font: %s)", properties_path, font_path) |
| 221 | + return properties_path |
| 222 | + |
| 223 | + |
157 | 224 | def resolve_plantuml_command(required: bool = True, graphviz_dot_path: Optional[str] = None) -> Optional[str]: |
158 | 225 | """Build the full `plantuml` conf.py setting. |
159 | 226 |
|
160 | | - Combines PLANTUML_BIN with the FTA metamodel include path and the |
161 | | - hermetic Graphviz dot, exactly matching the default template's |
162 | | - configuration. Pair this with `plantuml_output_format = "svg_obj"` in |
163 | | - conf.py (a fixed literal, not tool-path dependent, so it isn't derived |
164 | | - here). |
| 227 | + Combines PLANTUML_BIN with the FTA metamodel include path, the hermetic |
| 228 | + Graphviz dot, and the hermetic fontconfig fallback, exactly matching the |
| 229 | + default template's configuration. Pair this with |
| 230 | + `plantuml_output_format = "svg_obj"` in conf.py (a fixed literal, not |
| 231 | + tool-path dependent, so it isn't derived here). |
165 | 232 |
|
166 | 233 | Args: |
167 | 234 | required: See `resolve_graphviz_dot`. Also governs whether a missing |
@@ -195,10 +262,13 @@ def resolve_plantuml_command(required: bool = True, graphviz_dot_path: Optional[ |
195 | 262 | fta_dir = resolve_fta_metamodel_dir() |
196 | 263 | include_flag = " --jvm_flag=-Dplantuml.include.path=%s" % fta_dir if fta_dir else "" |
197 | 264 |
|
| 265 | + fontconfig_properties = resolve_plantuml_fontconfig() |
| 266 | + fontconfig_flag = " --jvm_flag=-Dsun.awt.fontconfig=%s" % fontconfig_properties if fontconfig_properties else "" |
| 267 | + |
198 | 268 | dot_path = graphviz_dot_path if graphviz_dot_path is not None else resolve_graphviz_dot(required=required) |
199 | 269 | layout_flag = " -graphvizdot %s" % dot_path if dot_path else "" |
200 | 270 |
|
201 | | - return "%s%s%s" % (plantuml_path, include_flag, layout_flag) |
| 271 | + return "%s%s%s%s" % (plantuml_path, include_flag, fontconfig_flag, layout_flag) |
202 | 272 |
|
203 | 273 |
|
204 | 274 | def init_hermetic_tools(app: Any, config: Any) -> None: |
|
0 commit comments