Skip to content

Commit 9a61f42

Browse files
Ehrjacastler
authored andcommitted
Provide bazel-maintained fallback font for PlantUML-parsing
Fundamentally driven via GH-copilot: Claude Sonnet 5
1 parent 4207105 commit 9a61f42

7 files changed

Lines changed: 290 additions & 17 deletions

File tree

‎bazel/rules/rules_score/docs/tooling_architecture.rst‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,26 @@ environment variables:
407407
PLANTUML_BIN_RLOC = ctx.executable._plantuml.short_path (rlocation key)
408408
GRAPHVIZ_DOT = ctx.executable._graphviz.path (execroot-relative)
409409
GRAPHVIZ_DOT_RLOC = ctx.executable._graphviz.short_path (rlocation key)
410+
FTA_METAMODEL_DIR = ctx.files._fta_metamodel[0].dirname (execroot-relative)
411+
PLANTUML_FONTCONFIG_DIR = ctx.files._plantuml_fontconfig[0].dirname (execroot-relative)
412+
413+
``FTA_METAMODEL_DIR`` and ``PLANTUML_FONTCONFIG_DIR`` only need the
414+
execroot-relative directory (no ``*_RLOC`` variant); they aren't executables
415+
so there's no runfiles-manifest entry to key a lookup on, and Python resolves
416+
their contents by simple ``os.path.join`` once the directory itself has been
417+
made absolute (see below).
418+
419+
``PLANTUML_FONTCONFIG_DIR`` points at
420+
``//third_party/plantuml:fontconfig_fallback``: a ``fontconfig.properties.tpl``
421+
template plus a bundled ``LiberationSans-Regular.ttf`` font.
422+
``sphinx_conf_helpers.resolve_plantuml_fontconfig()`` substitutes the font's
423+
absolute path into the template, writes the result to a temp file, and
424+
``resolve_plantuml_command()`` passes it to PlantUML via
425+
``--jvm_flag=-Dsun.awt.fontconfig=<path>``. This makes
426+
``sun.awt.X11FontManager`` use the bundled font directly instead of querying
427+
the native libfontconfig library / host-installed fonts, which otherwise
428+
makes PlantUML crash with ``Fontconfig head is null, check your fonts or
429+
fonts configuration`` in minimal containers/toolchains that have neither.
410430

411431
The rlocation keys (``*_RLOC``) are computed once at analysis time:
412432

‎bazel/rules/rules_score/private/sphinx_module.bzl‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ sphinx_rule_attrs = dict(
149149
doc = "Directory containing fta_metamodel.puml, passed to PlantUML via " +
150150
"-Dplantuml.include.path so FTA diagrams can resolve !include fta_metamodel.puml.",
151151
),
152+
"_plantuml_fontconfig": attr.label(
153+
default = Label("//third_party/plantuml:fontconfig_fallback"),
154+
allow_files = True,
155+
doc = "Directory containing fontconfig.properties.tpl and the bundled " +
156+
"LiberationSans-Regular.ttf fallback font, passed to PlantUML via " +
157+
"-Dsun.awt.fontconfig so it gets usable text metrics even when the " +
158+
"execution environment has no native fontconfig library/fonts.",
159+
),
152160
"allow_persistent_workers": attr.bool(
153161
default = False,
154162
doc = "(experimental) If true, allow Bazel to run this pass's Sphinx build " +
@@ -233,19 +241,28 @@ def _hermetic_tool_env(ctx):
233241
start, while cwd is still the execroot) and an analysis-time-stable
234242
rlocation key (no exec-config hash) for diagnostic logging. See
235243
docs/tooling_architecture.rst §"Hermetic tool path resolution".
244+
245+
The returned files list (first return value) must be added to the calling
246+
action's `inputs` -- it covers both the FTA metamodel include and the
247+
PlantUML fontconfig fallback (font + template), neither of which is
248+
otherwise reachable from the `tools` attr's executables alone.
236249
"""
237250
gv_short = ctx.executable._graphviz.short_path
238251
graphviz_rloc = gv_short[3:] if gv_short.startswith("../") else ctx.workspace_name + "/" + gv_short
239252
pl_short = ctx.executable._plantuml.short_path
240253
plantuml_rloc = pl_short[3:] if pl_short.startswith("../") else ctx.workspace_name + "/" + pl_short
241254
fta_metamodel_files = ctx.files._fta_metamodel
242255
fta_metamodel_dir = fta_metamodel_files[0].dirname if fta_metamodel_files else ""
243-
return fta_metamodel_files, {
256+
fontconfig_files = ctx.files._plantuml_fontconfig
257+
fontconfig_dir = fontconfig_files[0].dirname if fontconfig_files else ""
258+
hermetic_files = fta_metamodel_files + fontconfig_files
259+
return hermetic_files, {
244260
"PLANTUML_BIN": ctx.executable._plantuml.path,
245261
"PLANTUML_BIN_RLOC": plantuml_rloc,
246262
"GRAPHVIZ_DOT": ctx.executable._graphviz.path,
247263
"GRAPHVIZ_DOT_RLOC": graphviz_rloc,
248264
"FTA_METAMODEL_DIR": fta_metamodel_dir,
265+
"PLANTUML_FONTCONFIG_DIR": fontconfig_dir,
249266
}
250267

251268
def _needs_output_prefix(name):
@@ -309,9 +326,9 @@ def _score_needs_impl(ctx):
309326
worker_enabled = worker_enabled,
310327
)
311328

312-
fta_metamodel_files, action_env = _hermetic_tool_env(ctx)
329+
hermetic_tool_files, action_env = _hermetic_tool_env(ctx)
313330
ctx.actions.run(
314-
inputs = needs_inputs + fta_metamodel_files,
331+
inputs = needs_inputs + hermetic_tool_files,
315332
outputs = [needs_output],
316333
arguments = [args],
317334
env = action_env,
@@ -506,10 +523,10 @@ def _score_html_impl(ctx):
506523

507524
# Use the hermetic graphviz wrapper that executes `/usr/bin/dot` inside the
508525
# docs_runtime sysroot via exec_in_sysroot.
509-
fta_metamodel_files, action_env = _hermetic_tool_env(ctx)
526+
hermetic_tool_files, action_env = _hermetic_tool_env(ctx)
510527

511528
ctx.actions.run(
512-
inputs = html_inputs + fta_metamodel_files,
529+
inputs = html_inputs + hermetic_tool_files,
513530
outputs = [sphinx_html_output],
514531
arguments = [args],
515532
env = action_env,

‎bazel/rules/rules_score/src/sphinx_conf_helpers.py‎

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
theme, etc.):
2121
2222
- 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".
2929
- sphinx-needs external-needs loading, re-exported from bazel_sphinx_needs
3030
rather than re-derived (see that module's docstring for the JSON format).
3131
- The sphinx-needs type/option/link schema loaded from the upstream S-CORE
@@ -36,6 +36,7 @@
3636

3737
import logging
3838
import os
39+
import tempfile
3940
from typing import Any, Dict, List, Optional
4041

4142
from bazel_sphinx_needs import (
@@ -154,14 +155,80 @@ def resolve_fta_metamodel_dir() -> str:
154155
return resolved
155156

156157

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+
157224
def resolve_plantuml_command(required: bool = True, graphviz_dot_path: Optional[str] = None) -> Optional[str]:
158225
"""Build the full `plantuml` conf.py setting.
159226
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).
165232
166233
Args:
167234
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[
195262
fta_dir = resolve_fta_metamodel_dir()
196263
include_flag = " --jvm_flag=-Dplantuml.include.path=%s" % fta_dir if fta_dir else ""
197264

265+
fontconfig_properties = resolve_plantuml_fontconfig()
266+
fontconfig_flag = " --jvm_flag=-Dsun.awt.fontconfig=%s" % fontconfig_properties if fontconfig_properties else ""
267+
198268
dot_path = graphviz_dot_path if graphviz_dot_path is not None else resolve_graphviz_dot(required=required)
199269
layout_flag = " -graphvizdot %s" % dot_path if dot_path else ""
200270

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)
202272

203273

204274
def init_hermetic_tools(app: Any, config: Any) -> None:

‎third_party/plantuml/BUILD‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,24 @@ java_binary(
2121
"@blueprint_maven_dependencies//:net_sourceforge_plantuml_plantuml",
2222
],
2323
)
24+
25+
# Fallback font + sun.awt.FontConfiguration template so PlantUML gets usable
26+
# text metrics even when the execution environment has no native fontconfig
27+
# library and/or no installed fonts at all (e.g. a minimal Docker build
28+
# image). Without this, the JVM's AWT font manager fails hard the first time
29+
# PlantUML asks for a font metric ("Fontconfig head is null, check your
30+
# fonts or fonts configuration") regardless of -Djava.awt.headless.
31+
# Consumed by sphinx_module.bzl's _hermetic_tool_env() /
32+
# sphinx_conf_helpers.resolve_plantuml_fontconfig(), which resolves
33+
# fontconfig.properties.tpl's {font_path} placeholder to
34+
# LiberationSans-Regular.ttf's absolute runtime path and passes the result
35+
# via -Dsun.awt.fontconfig.
36+
# See LiberationSans-LICENSE.txt for the font's (SIL OFL 1.1) license.
37+
filegroup(
38+
name = "fontconfig_fallback",
39+
srcs = [
40+
"LiberationSans-Regular.ttf",
41+
"fontconfig.properties.tpl",
42+
],
43+
visibility = ["//visibility:public"],
44+
)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
Liberation Sans (LiberationSans-Regular.ttf) is vendored here as a fallback
2+
font for sun.awt.FontConfiguration (see fontconfig.properties.tpl and
3+
sphinx_conf_helpers.resolve_plantuml_fontconfig()), so PlantUML gets usable
4+
text metrics even in environments without a native fontconfig library or
5+
any installed fonts.
6+
7+
Source: https://github.com/liberationfonts
8+
Upstream-Name: Liberation Fonts
9+
10+
Copyright:
11+
Digitized data copyright (c) 2010 Google Corporation with Reserved Font
12+
Name Arimo, Tinos and Cousine.
13+
Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation.
14+
15+
Licensed under the SIL Open Font License, Version 1.1, reproduced below and
16+
also available at https://openfontlicense.org / http://scripts.sil.org/OFL.
17+
18+
-----------------------------------------------------------
19+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
20+
-----------------------------------------------------------
21+
22+
PREAMBLE
23+
The goals of the Open Font License (OFL) are to stimulate worldwide
24+
development of collaborative font projects, to support the font creation
25+
efforts of academic and linguistic communities, and to provide a free and
26+
open framework in which fonts may be shared and improved in partnership
27+
with others.
28+
29+
The OFL allows the licensed fonts to be used, studied, modified and
30+
redistributed freely as long as they are not sold by themselves. The
31+
fonts, including any derivative works, can be bundled, embedded,
32+
redistributed and/or sold with any software provided that any reserved
33+
names are not used by derivative works. The fonts and derivatives,
34+
however, cannot be released under any other type of license. The
35+
requirement for fonts to remain under this license does not apply to any
36+
document created using the fonts or their derivatives.
37+
38+
DEFINITIONS
39+
"Font Software" refers to the set of files released by the Copyright
40+
Holder(s) under this license and clearly marked as such. This may
41+
include source files, build scripts and documentation.
42+
43+
"Reserved Font Name" refers to any names specified as such after the
44+
copyright statement(s).
45+
46+
"Original Version" refers to the collection of Font Software components as
47+
distributed by the Copyright Holder(s).
48+
49+
"Modified Version" refers to any derivative made by adding to, deleting,
50+
or substituting -- in part or in whole -- any of the components of the
51+
Original Version, by changing formats or by porting the Font Software to a
52+
new environment.
53+
54+
"Author" refers to any designer, engineer, programmer, technical writer or
55+
other person who contributed to the Font Software.
56+
57+
PERMISSION & CONDITIONS
58+
Permission is hereby granted, free of charge, to any person obtaining a
59+
copy of the Font Software, to use, study, copy, merge, embed, modify,
60+
redistribute, and sell modified and unmodified copies of the Font
61+
Software, subject to the following conditions:
62+
63+
1) Neither the Font Software nor any of its individual components, in
64+
Original or Modified Versions, may be sold by itself.
65+
66+
2) Original or Modified Versions of the Font Software may be bundled,
67+
redistributed and/or sold with any software, provided that each copy
68+
contains the above copyright notice and this license. These can be
69+
included either as stand-alone text files, human-readable headers or
70+
in the appropriate machine-readable metadata fields within text or
71+
binary files as long as those fields can be easily viewed by the user.
72+
73+
3) No Modified Version of the Font Software may use the Reserved Font
74+
Name(s) unless explicit written permission is granted by the
75+
corresponding Copyright Holder. This restriction only applies to the
76+
primary font name as presented to the users.
77+
78+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
79+
Software shall not be used to promote, endorse or advertise any
80+
Modified Version, except to acknowledge the contribution(s) of the
81+
Copyright Holder(s) and the Author(s) or with their explicit written
82+
permission.
83+
84+
5) The Font Software, modified or unmodified, in part or in whole, must be
85+
distributed entirely under this license, and must not be distributed
86+
under any other license. The requirement for fonts to remain under
87+
this license does not apply to any document created using the Font
88+
Software.
89+
90+
TERMINATION
91+
This license becomes null and void if any of the above conditions are not
92+
met.
93+
94+
DISCLAIMER
95+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
96+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
97+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
98+
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
99+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
100+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
101+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
102+
OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
103+
DEALINGS IN THE FONT SOFTWARE.
401 KB
Binary file not shown.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
#
14+
# Minimal sun.awt.FontConfiguration properties file, mapping every logical
15+
# Java font (Serif, SansSerif, Monospaced, Dialog, DialogInput) to the single
16+
# bundled LiberationSans-Regular.ttf fallback font.
17+
#
18+
# Why this exists: OpenJDK on Linux normally builds its logical-font mapping
19+
# by querying the native libfontconfig library and the host's installed
20+
# fonts. In a minimal container/toolchain that has neither, that query fails
21+
# and BOTH the native path and this file's absence cause
22+
# sun.awt.FontConfiguration to throw "Fontconfig head is null, check your
23+
# fonts or fonts configuration" the first time any AWT font metric is
24+
# requested (see PlantUML's Run.forceOpenJdkResourceLoad, which calls
25+
# Font.getStringBounds() specifically to surface this early). Pointing the
26+
# JVM at this file via -Dsun.awt.fontconfig=<resolved path> makes
27+
# sun.awt.X11FontManager use it directly instead of querying the native
28+
# library, so PlantUML gets usable (if visually approximate) text metrics
29+
# regardless of what fonts, if any, the host/container provides.
30+
#
31+
# {font_path} is substituted at Sphinx-config time (see
32+
# sphinx_conf_helpers.resolve_plantuml_fontconfig) with the absolute,
33+
# execroot-resolved path to the bundled LiberationSans-Regular.ttf runfile -- it cannot
34+
# be a path literal here because the actual on-disk location depends on the
35+
# Bazel sandbox/runfiles layout of whichever action executes PlantUML.
36+
version=1
37+
38+
sequence.allfonts=default
39+
40+
allfonts.default=default
41+
42+
filename.default={font_path}

0 commit comments

Comments
 (0)