Skip to content

Commit 9291581

Browse files
committed
[rules score] rework dot integration
- remove graphviz directive support - change lobster report to plantuml directive - use dot for rendering in plantuml (smetana as fallback)
1 parent 88418e5 commit 9291581

11 files changed

Lines changed: 206 additions & 274 deletions

File tree

bazel/rules/rules_score/private/sphinx_module.bzl

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -238,29 +238,15 @@ def _score_html_impl(ctx):
238238
get_log_level(ctx),
239239
]
240240

241-
# Wire in the hermetic graphviz deb (dot_builtins + bundled shared libs) if provided.
242-
# conf.template.py resolves all three env vars (GRAPHVIZ_DOT,
243-
# LD_LIBRARY_PATH, LTDL_LIBRARY_PATH) from execroot-relative to absolute
244-
# paths so dot_builtins can load its plugins without a system installation.
241+
# Wire in the hermetic graphviz deb if provided. GRAPHVIZ_DOT points at
242+
# dot_builtins; conf.template.py resolves it to an absolute path and derives
243+
# LD_LIBRARY_PATH / LTDL_LIBRARY_PATH from it using stdlib Path.parent so
244+
# the rule itself needs no knowledge of the deb's directory layout.
245245
graphviz_env = {}
246-
graphviz_files = ctx.files.graphviz
247-
if graphviz_files:
248-
_dot_suffix = "/usr/bin/dot_builtins"
249-
dot_binary = None
250-
for f in graphviz_files:
251-
if f.path.endswith(_dot_suffix):
252-
dot_binary = f
253-
break
254-
if not dot_binary:
255-
fail("graphviz target {} must provide usr/bin/dot_builtins".format(ctx.attr.graphviz))
256-
257-
graphviz_prefix = dot_binary.path[:-len(_dot_suffix)]
258-
graphviz_env = {
259-
"GRAPHVIZ_DOT": dot_binary.path,
260-
"LD_LIBRARY_PATH": graphviz_prefix + "/usr/lib",
261-
"LTDL_LIBRARY_PATH": graphviz_prefix + "/usr/lib/graphviz",
262-
}
263-
html_inputs = html_inputs + graphviz_files
246+
dot_binary = ctx.file.graphviz_dot
247+
if dot_binary:
248+
graphviz_env = {"GRAPHVIZ_DOT": dot_binary.path}
249+
html_inputs = html_inputs + [dot_binary] + ctx.files.graphviz_all
264250

265251
ctx.actions.run(
266252
inputs = html_inputs,
@@ -358,12 +344,20 @@ _score_html = rule(
358344
"destination paths relative to the Sphinx source root. Exactly one " +
359345
"file per label. Mirrors sphinx_docs.renamed_srcs from rules_python.",
360346
),
361-
graphviz = attr.label(
347+
graphviz_dot = attr.label(
348+
default = None,
349+
allow_single_file = True,
350+
doc = "Hermetic 'dot_builtins' binary from @graphviz_deb (e.g. @graphviz_deb//:dot_binary). " +
351+
"When set, PlantUML uses it via -graphvizdot for native Graphviz layout quality. " +
352+
"Only available on Linux x86_64; other platforms fall back to Smetana.",
353+
),
354+
graphviz_all = attr.label(
362355
default = None,
363356
allow_files = True,
364-
doc = "Graphviz cmake-release deb files (dot_builtins binary + bundled libs). " +
365-
"Only available on Linux x86_64; provides a hermetic 'dot' binary without requiring a system graphviz installation. " +
366-
"Defaults to @graphviz_deb//:all on Linux x86_64.",
357+
doc = "All Graphviz files from @graphviz_deb (e.g. @graphviz_deb//:all): the dot_builtins " +
358+
"binary, core shared libraries, and plugin shared libraries. These are staged into " +
359+
"the sandbox as action inputs; library directories are derived from graphviz_dot's " +
360+
"path using the standard FHS layout (usr/lib, usr/lib/graphviz).",
367361
),
368362
),
369363
toolchains = ["//bazel/rules/rules_score:toolchain_type"],
@@ -408,14 +402,21 @@ def sphinx_module(
408402
extra_opts_targets: {type}`list[label]` Label targets that resolve to extra Sphinx
409403
arguments at analysis time. Each target must provide FilteredExecpathInfo
410404
(e.g. filter_execpath targets).
411-
graphviz: Graphviz cmake-release deb files (dot_builtins + bundled libs). On Linux x86_64,
412-
defaults to @graphviz_deb//:all for hermetic graphviz support. On other platforms
413-
or if explicitly set to None, no graphviz support is provided (the sphinx.ext.graphviz
414-
extension will not be available).
405+
graphviz: Controls hermetic dot support for PlantUML layout. `None` (default)
406+
auto-enables it on linux_x86_64 using @graphviz_deb (dot_builtins + bundled libs)
407+
so PlantUML uses native Graphviz layout quality. On other platforms PlantUML
408+
falls back to Smetana. Pass `False` to disable hermetic dot wiring entirely.
415409
visibility: Bazel visibility
416410
"""
417-
if graphviz == None:
418-
graphviz = select({
411+
if graphviz == False:
412+
graphviz_dot = None
413+
graphviz_all = None
414+
else:
415+
graphviz_dot = select({
416+
"//bazel/rules/rules_score:linux_x86_64": "@graphviz_deb//:dot_binary",
417+
"//conditions:default": None,
418+
})
419+
graphviz_all = select({
419420
"//bazel/rules/rules_score:linux_x86_64": "@graphviz_deb//:all",
420421
"//conditions:default": None,
421422
})
@@ -438,7 +439,8 @@ def sphinx_module(
438439
needs = [d + "_needs" for d in deps],
439440
extra_opts = extra_opts,
440441
extra_opts_targets = extra_opts_targets,
441-
graphviz = graphviz,
442+
graphviz_dot = graphviz_dot,
443+
graphviz_all = graphviz_all,
442444
testonly = testonly,
443445
**kwargs
444446
)

bazel/rules/rules_score/src/sphinx_wrapper.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ def main() -> int:
284284
Exit code (0 for success, non-zero for failure)
285285
"""
286286
try:
287+
# Capture the Bazel execroot (the action's cwd) before Sphinx changes
288+
# into the generated source tree. conf.template.py reads
289+
# SPHINX_BAZEL_EXECROOT to resolve execroot-relative tool paths (e.g.
290+
# the hermetic graphviz dot binary) to absolute paths that stay valid
291+
# after Sphinx chdirs.
292+
os.environ["SPHINX_BAZEL_EXECROOT"] = os.getcwd()
287293
args, extra_args = parse_arguments()
288294
logging.basicConfig(
289295
level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s"

bazel/rules/rules_score/templates/conf.template.py

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -35,42 +35,30 @@
3535
# ---------------------------------------------------------------------------
3636

3737

38-
# Capture the current working directory at module import time.
39-
# In Bazel action context, cwd == execroot. In IDE/non-Bazel runs, cwd is
40-
# the current directory. This is captured once to avoid repeated resolution.
41-
_EXECROOT = Path.cwd()
38+
# Resolve execroot-relative paths against the Bazel execroot. sphinx_wrapper.py
39+
# exports SPHINX_BAZEL_EXECROOT (the action cwd captured before Sphinx changes
40+
# into the generated source tree). Fall back to the current cwd for non-Bazel
41+
# / IDE runs where the variable is absent.
42+
_EXECROOT = Path(os.environ.get("SPHINX_BAZEL_EXECROOT", "") or Path.cwd())
4243

4344

4445
def _resolve_execroot_path(path_value: str) -> str:
4546
"""Resolve an execroot-relative path to an absolute filesystem path.
4647
4748
Bazel passes action inputs as paths relative to the execroot (e.g.
48-
``external/+_repo_rules2+graphviz_deb/usr/bin/dot_builtins``). Those
49-
paths are only valid when the process' cwd is the execroot — which is
50-
not guaranteed once Sphinx changes directories during the build.
49+
``external/+_repo_rules+graphviz_deb/usr/bin/dot_builtins``). Sphinx changes
50+
into the generated source tree before importing conf.py, so the process cwd
51+
is no longer the execroot. We resolve against ``_EXECROOT`` (captured by the
52+
wrapper before that chdir) so the paths stay valid for the ``dot``
53+
subprocess.
5154
52-
This function makes them absolute so they work regardless of cwd.
5355
Absolute paths and plain command names (e.g. ``dot``) are returned
5456
unchanged.
5557
"""
5658
p = Path(path_value)
5759
if p.is_absolute():
5860
return str(p)
5961
if path_value.startswith("external/") or path_value.startswith("bazel-out/"):
60-
# First try cwd-as-execroot (fast path).
61-
candidate = (_EXECROOT / p).resolve()
62-
if candidate.exists():
63-
return str(candidate)
64-
65-
# If cwd is nested under bazel-out, walk upward and locate the first
66-
# parent that contains the requested relative path.
67-
for parent in [_EXECROOT, *_EXECROOT.parents]:
68-
candidate = (parent / p).resolve()
69-
if candidate.exists():
70-
return str(candidate)
71-
72-
# Fallback: preserve previous behavior even if the file does not exist
73-
# yet (keeps logging/debug output deterministic).
7462
return str((_EXECROOT / p).resolve())
7563
return path_value
7664

@@ -100,7 +88,6 @@ def _resolve_execroot_path(path_value: str) -> str:
10088
"sphinxcontrib.plantuml",
10189
"trlc",
10290
"clickable_plantuml",
103-
"sphinx.ext.graphviz",
10491
]
10592

10693
# MyST parser extensions
@@ -205,38 +192,36 @@ def _resolve_execroot_path(path_value: str) -> str:
205192
f"Could not find plantuml binary via runfiles lookup. Searched: {searched}."
206193
)
207194

208-
# Use PlantUML's built-in Smetana layout engine (Java port of Graphviz).
209-
# This avoids requiring an external dot binary in the Bazel sandbox.
210-
plantuml = f"{plantuml_path} -Playout=smetana"
211-
plantuml_output_format = "svg_obj"
212-
213195
# ---------------------------------------------------------------------------
214-
# Graphviz (sphinx.ext.graphviz)
196+
# PlantUML + hermetic dot
215197
# ---------------------------------------------------------------------------
216-
# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the hermetic
217-
# dot_builtins binary from @graphviz_deb. The path is execroot-relative, so
218-
# we resolve it to an absolute path here so it remains valid after any cwd
219-
# change that Sphinx may perform during the build.
220-
# If GRAPHVIZ_DOT is absent, force a known-invalid dot path so Sphinx fails
221-
# clearly on graphviz directives instead of silently using host-installed dot.
198+
# GRAPHVIZ_DOT is set by sphinx_module on linux_x86_64 to the hermetic
199+
# dot_builtins binary from @graphviz_deb. When present, PlantUML is told to
200+
# use it directly via -graphvizdot, giving native Graphviz layout quality for
201+
# all UML diagram types. LD_LIBRARY_PATH / LTDL_LIBRARY_PATH are resolved to
202+
# absolute paths here so they remain valid in the dot_builtins subprocess that
203+
# PlantUML spawns (Sphinx may have chdir'd before then).
204+
# On other platforms (e.g. arm64, macOS) GRAPHVIZ_DOT is absent and PlantUML
205+
# falls back to its built-in Smetana layout engine (pure-Java, no dot needed).
222206
if "GRAPHVIZ_DOT" in os.environ:
223-
graphviz_dot = _resolve_execroot_path(os.environ["GRAPHVIZ_DOT"])
224-
graphviz_output_format = "svg"
225-
226-
# LD_LIBRARY_PATH and LTDL_LIBRARY_PATH are set by the Bazel rule as
227-
# execroot-relative paths. We mutate os.environ (not just a local) because
228-
# sphinx.ext.graphviz spawns `dot` as a child process that inherits these
229-
# variables to locate the bundled shared libraries and plugins. Each
230-
# component is resolved to absolute so it stays valid if Sphinx changes cwd
231-
# before spawning the dot subprocess.
232-
for _env_var in ("LD_LIBRARY_PATH", "LTDL_LIBRARY_PATH"):
233-
_env_val = os.environ.get(_env_var, "")
234-
if _env_val:
235-
os.environ[_env_var] = ":".join(
236-
_resolve_execroot_path(p) for p in _env_val.split(":")
237-
)
207+
_dot_path = Path(_resolve_execroot_path(os.environ["GRAPHVIZ_DOT"]))
208+
# Derive library search paths from the binary location so the rule passes
209+
# only GRAPHVIZ_DOT and conf.py stays self-contained.
210+
# The graphviz cmake deb installs:
211+
# usr/bin/dot_builtins ← GRAPHVIZ_DOT points here
212+
# usr/lib/*.so* ← LD_LIBRARY_PATH (core shared libs)
213+
# usr/lib/graphviz/*.so* ← LTDL_LIBRARY_PATH (layout/render plugins)
214+
_usr_dir = _dot_path.parent.parent # usr/bin → parent → usr
215+
os.environ["LD_LIBRARY_PATH"] = str(_usr_dir / "lib")
216+
os.environ["LTDL_LIBRARY_PATH"] = str(_usr_dir / "lib" / "graphviz")
217+
plantuml = f"{plantuml_path} -graphvizdot {_dot_path}"
238218
else:
239-
graphviz_dot = "/__hermetic_graphviz_not_configured__/dot"
219+
logger.warning(
220+
"GRAPHVIZ_DOT not set; PlantUML falling back to Smetana layout engine. "
221+
"Hermetic dot (@graphviz_deb) is only available on linux_x86_64."
222+
)
223+
plantuml = f"{plantuml_path} -Playout=smetana"
224+
plantuml_output_format = "svg_obj"
240225

241226
# HTML theme
242227
html_theme = "sphinx_rtd_theme"

bazel/rules/rules_score/test/BUILD

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,13 @@ sphinx_module(
126126
],
127127
)
128128

129-
# Test 2: Graphviz Rendering
130-
# Tests hermetic graphviz support via sphinx.ext.graphviz directive
129+
# Test 2: PlantUML dot rendering
130+
# Tests that the hermetic dot binary is wired to PlantUML via -graphvizdot
131+
# and that @startdot content in a .. uml:: directive renders to SVG.
131132
sphinx_module(
132-
name = "graphviz_test_lib",
133-
srcs = glob(["fixtures/graphviz_test/*.rst"]),
134-
index = "fixtures/graphviz_test/index.rst",
133+
name = "plantuml_dot_test_lib",
134+
srcs = glob(["fixtures/plantuml_dot_test/*.rst"]),
135+
index = "fixtures/plantuml_dot_test/index.rst",
135136
sphinx = "@score_tooling//bazel/rules/rules_score:score_build",
136137
)
137138

@@ -788,11 +789,11 @@ py_test(
788789
)
789790

790791
py_test(
791-
name = "test_graphviz_rendering",
792+
name = "test_plantuml_dot_rendering",
792793
size = "small",
793-
srcs = ["graphviz_render_test.py"],
794-
data = [":graphviz_test_lib"],
795-
main = "graphviz_render_test.py",
794+
srcs = ["plantuml_dot_render_test.py"],
795+
data = [":plantuml_dot_test_lib"],
796+
main = "plantuml_dot_render_test.py",
796797
)
797798

798799
# Combined test suite for all tests
@@ -805,6 +806,7 @@ test_suite(
805806
":sphinx_module_tests",
806807
":test_aou_forwarding_to_lobster",
807808
":test_graphviz_rendering",
809+
":test_plantuml_dot_rendering",
808810
":test_rst_to_trlc",
809811
":test_safety_analysis_tools",
810812
":test_trlc_rst_image_rendering",

bazel/rules/rules_score/test/fixtures/graphviz_test/index.rst renamed to bazel/rules/rules_score/test/fixtures/plantuml_dot_test/index.rst

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
..
22
# *******************************************************************************
3-
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
# Copyright (c) 2025 Contributors to the Eclipse Foundation
44
#
55
# See the NOTICE file(s) distributed with this work for additional
66
# information regarding copyright ownership.
@@ -12,24 +12,26 @@
1212
# SPDX-License-Identifier: Apache-2.0
1313
# *******************************************************************************
1414
15-
Graphviz Rendering Test
16-
=======================
15+
PlantUML dot Rendering Test
16+
===========================
1717

18-
This document tests the ``sphinx.ext.graphviz`` directive to ensure hermetic graphviz
19-
integration is working correctly.
18+
This document verifies that the hermetic ``dot_builtins`` binary is correctly
19+
wired to PlantUML via ``-graphvizdot`` and that ``@startdot`` content inside a
20+
``.. uml::`` directive is rendered as an SVG image.
2021

21-
Simple DAG
22-
----------
22+
Simple DAG via @startdot
23+
------------------------
2324

24-
.. graphviz::
25-
:align: center
25+
.. uml::
2626

27+
@startdot
2728
digraph {
2829
A -> B;
2930
A -> C;
3031
B -> D;
3132
C -> D;
3233
label = "Simple DAG";
3334
}
35+
@enddot
3436

35-
This graphviz diagram should render as SVG in the produced HTML output.
37+
The diagram above should appear as an SVG in the produced HTML output.

0 commit comments

Comments
 (0)