diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl
index 1a2007f4..3278d8bd 100644
--- a/bazel/rules/rules_score/private/sphinx_module.bzl
+++ b/bazel/rules/rules_score/private/sphinx_module.bzl
@@ -238,29 +238,15 @@ def _score_html_impl(ctx):
get_log_level(ctx),
]
- # Wire in the hermetic graphviz deb (dot_builtins + bundled shared libs) if provided.
- # conf.template.py resolves all three env vars (GRAPHVIZ_DOT,
- # LD_LIBRARY_PATH, LTDL_LIBRARY_PATH) from execroot-relative to absolute
- # paths so dot_builtins can load its plugins without a system installation.
+ # Wire in the hermetic graphviz deb if provided. GRAPHVIZ_DOT points at
+ # dot_builtins; conf.template.py resolves it to an absolute path and derives
+ # LD_LIBRARY_PATH / LTDL_LIBRARY_PATH from it using stdlib Path.parent so
+ # the rule itself needs no knowledge of the deb's directory layout.
graphviz_env = {}
- graphviz_files = ctx.files.graphviz
- if graphviz_files:
- _dot_suffix = "/usr/bin/dot_builtins"
- dot_binary = None
- for f in graphviz_files:
- if f.path.endswith(_dot_suffix):
- dot_binary = f
- break
- if not dot_binary:
- fail("graphviz target {} must provide usr/bin/dot_builtins".format(ctx.attr.graphviz))
-
- graphviz_prefix = dot_binary.path[:-len(_dot_suffix)]
- graphviz_env = {
- "GRAPHVIZ_DOT": dot_binary.path,
- "LD_LIBRARY_PATH": graphviz_prefix + "/usr/lib",
- "LTDL_LIBRARY_PATH": graphviz_prefix + "/usr/lib/graphviz",
- }
- html_inputs = html_inputs + graphviz_files
+ dot_binary = ctx.file.graphviz_dot
+ if dot_binary:
+ graphviz_env = {"GRAPHVIZ_DOT": dot_binary.path}
+ html_inputs = html_inputs + [dot_binary] + ctx.files.graphviz_all
ctx.actions.run(
inputs = html_inputs,
@@ -358,12 +344,20 @@ _score_html = rule(
"destination paths relative to the Sphinx source root. Exactly one " +
"file per label. Mirrors sphinx_docs.renamed_srcs from rules_python.",
),
- graphviz = attr.label(
+ graphviz_dot = attr.label(
+ default = None,
+ allow_single_file = True,
+ doc = "Hermetic 'dot_builtins' binary from @graphviz_deb (e.g. @graphviz_deb//:dot_binary). " +
+ "When set, PlantUML uses it via -graphvizdot for native Graphviz layout quality. " +
+ "Only available on Linux x86_64; other platforms fall back to Smetana.",
+ ),
+ graphviz_all = attr.label(
default = None,
allow_files = True,
- doc = "Graphviz cmake-release deb files (dot_builtins binary + bundled libs). " +
- "Only available on Linux x86_64; provides a hermetic 'dot' binary without requiring a system graphviz installation. " +
- "Defaults to @graphviz_deb//:all on Linux x86_64.",
+ doc = "All Graphviz files from @graphviz_deb (e.g. @graphviz_deb//:all): the dot_builtins " +
+ "binary, core shared libraries, and plugin shared libraries. These are staged into " +
+ "the sandbox as action inputs; library directories are derived from graphviz_dot's " +
+ "path using the standard FHS layout (usr/lib, usr/lib/graphviz).",
),
),
toolchains = ["//bazel/rules/rules_score:toolchain_type"],
@@ -408,14 +402,21 @@ def sphinx_module(
extra_opts_targets: {type}`list[label]` Label targets that resolve to extra Sphinx
arguments at analysis time. Each target must provide FilteredExecpathInfo
(e.g. filter_execpath targets).
- graphviz: Graphviz cmake-release deb files (dot_builtins + bundled libs). On Linux x86_64,
- defaults to @graphviz_deb//:all for hermetic graphviz support. On other platforms
- or if explicitly set to None, no graphviz support is provided (the sphinx.ext.graphviz
- extension will not be available).
+ graphviz: Controls hermetic dot support for PlantUML layout. `None` (default)
+ auto-enables it on linux_x86_64 using @graphviz_deb (dot_builtins + bundled libs)
+ so PlantUML uses native Graphviz layout quality. On other platforms PlantUML
+ falls back to Smetana. Pass `False` to disable hermetic dot wiring entirely.
visibility: Bazel visibility
"""
- if graphviz == None:
- graphviz = select({
+ if graphviz == False:
+ graphviz_dot = None
+ graphviz_all = None
+ else:
+ graphviz_dot = select({
+ "//bazel/rules/rules_score:linux_x86_64": "@graphviz_deb//:dot_binary",
+ "//conditions:default": None,
+ })
+ graphviz_all = select({
"//bazel/rules/rules_score:linux_x86_64": "@graphviz_deb//:all",
"//conditions:default": None,
})
@@ -438,7 +439,8 @@ def sphinx_module(
needs = [d + "_needs" for d in deps],
extra_opts = extra_opts,
extra_opts_targets = extra_opts_targets,
- graphviz = graphviz,
+ graphviz_dot = graphviz_dot,
+ graphviz_all = graphviz_all,
testonly = testonly,
**kwargs
)
diff --git a/bazel/rules/rules_score/src/sphinx_wrapper.py b/bazel/rules/rules_score/src/sphinx_wrapper.py
index 760c7c39..9dc9971f 100644
--- a/bazel/rules/rules_score/src/sphinx_wrapper.py
+++ b/bazel/rules/rules_score/src/sphinx_wrapper.py
@@ -284,6 +284,12 @@ def main() -> int:
Exit code (0 for success, non-zero for failure)
"""
try:
+ # Capture the Bazel execroot (the action's cwd) before Sphinx changes
+ # into the generated source tree. conf.template.py reads
+ # SPHINX_BAZEL_EXECROOT to resolve execroot-relative tool paths (e.g.
+ # the hermetic graphviz dot binary) to absolute paths that stay valid
+ # after Sphinx chdirs.
+ os.environ["SPHINX_BAZEL_EXECROOT"] = os.getcwd()
args, extra_args = parse_arguments()
logging.basicConfig(
level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s"
diff --git a/bazel/rules/rules_score/templates/conf.template.py b/bazel/rules/rules_score/templates/conf.template.py
index df5964d1..a82cc849 100644
--- a/bazel/rules/rules_score/templates/conf.template.py
+++ b/bazel/rules/rules_score/templates/conf.template.py
@@ -35,21 +35,23 @@
# ---------------------------------------------------------------------------
-# Capture the current working directory at module import time.
-# In Bazel action context, cwd == execroot. In IDE/non-Bazel runs, cwd is
-# the current directory. This is captured once to avoid repeated resolution.
-_EXECROOT = Path.cwd()
+# Resolve execroot-relative paths against the Bazel execroot. sphinx_wrapper.py
+# exports SPHINX_BAZEL_EXECROOT (the action cwd captured before Sphinx changes
+# into the generated source tree). Fall back to the current cwd for non-Bazel
+# / IDE runs where the variable is absent.
+_EXECROOT = Path(os.environ.get("SPHINX_BAZEL_EXECROOT", "") or Path.cwd())
def _resolve_execroot_path(path_value: str) -> str:
"""Resolve an execroot-relative path to an absolute filesystem path.
Bazel passes action inputs as paths relative to the execroot (e.g.
- ``external/+_repo_rules2+graphviz_deb/usr/bin/dot_builtins``). Those
- paths are only valid when the process' cwd is the execroot — which is
- not guaranteed once Sphinx changes directories during the build.
+ ``external/+_repo_rules+graphviz_deb/usr/bin/dot_builtins``). Sphinx changes
+ into the generated source tree before importing conf.py, so the process cwd
+ is no longer the execroot. We resolve against ``_EXECROOT`` (captured by the
+ wrapper before that chdir) so the paths stay valid for the ``dot``
+ subprocess.
- This function makes them absolute so they work regardless of cwd.
Absolute paths and plain command names (e.g. ``dot``) are returned
unchanged.
"""
@@ -57,20 +59,6 @@ def _resolve_execroot_path(path_value: str) -> str:
if p.is_absolute():
return str(p)
if path_value.startswith("external/") or path_value.startswith("bazel-out/"):
- # First try cwd-as-execroot (fast path).
- candidate = (_EXECROOT / p).resolve()
- if candidate.exists():
- return str(candidate)
-
- # If cwd is nested under bazel-out, walk upward and locate the first
- # parent that contains the requested relative path.
- for parent in [_EXECROOT, *_EXECROOT.parents]:
- candidate = (parent / p).resolve()
- if candidate.exists():
- return str(candidate)
-
- # Fallback: preserve previous behavior even if the file does not exist
- # yet (keeps logging/debug output deterministic).
return str((_EXECROOT / p).resolve())
return path_value
@@ -100,7 +88,6 @@ def _resolve_execroot_path(path_value: str) -> str:
"sphinxcontrib.plantuml",
"trlc",
"clickable_plantuml",
- "sphinx.ext.graphviz",
]
# MyST parser extensions
@@ -205,38 +192,36 @@ def _resolve_execroot_path(path_value: str) -> str:
f"Could not find plantuml binary via runfiles lookup. Searched: {searched}."
)
-# Use PlantUML's built-in Smetana layout engine (Java port of Graphviz).
-# This avoids requiring an external dot binary in the Bazel sandbox.
-plantuml = f"{plantuml_path} -Playout=smetana"
-plantuml_output_format = "svg_obj"
-
# ---------------------------------------------------------------------------
-# Graphviz (sphinx.ext.graphviz)
+# PlantUML + hermetic dot
# ---------------------------------------------------------------------------
-# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the hermetic
-# dot_builtins binary from @graphviz_deb. The path is execroot-relative, so
-# we resolve it to an absolute path here so it remains valid after any cwd
-# change that Sphinx may perform during the build.
-# If GRAPHVIZ_DOT is absent, force a known-invalid dot path so Sphinx fails
-# clearly on graphviz directives instead of silently using host-installed dot.
+# GRAPHVIZ_DOT is set by sphinx_module on linux_x86_64 to the hermetic
+# dot_builtins binary from @graphviz_deb. When present, PlantUML is told to
+# use it directly via -graphvizdot, giving native Graphviz layout quality for
+# all UML diagram types. LD_LIBRARY_PATH / LTDL_LIBRARY_PATH are resolved to
+# absolute paths here so they remain valid in the dot_builtins subprocess that
+# PlantUML spawns (Sphinx may have chdir'd before then).
+# On other platforms (e.g. arm64, macOS) GRAPHVIZ_DOT is absent and PlantUML
+# falls back to its built-in Smetana layout engine (pure-Java, no dot needed).
if "GRAPHVIZ_DOT" in os.environ:
- graphviz_dot = _resolve_execroot_path(os.environ["GRAPHVIZ_DOT"])
- graphviz_output_format = "svg"
-
- # LD_LIBRARY_PATH and LTDL_LIBRARY_PATH are set by the Bazel rule as
- # execroot-relative paths. We mutate os.environ (not just a local) because
- # sphinx.ext.graphviz spawns `dot` as a child process that inherits these
- # variables to locate the bundled shared libraries and plugins. Each
- # component is resolved to absolute so it stays valid if Sphinx changes cwd
- # before spawning the dot subprocess.
- for _env_var in ("LD_LIBRARY_PATH", "LTDL_LIBRARY_PATH"):
- _env_val = os.environ.get(_env_var, "")
- if _env_val:
- os.environ[_env_var] = ":".join(
- _resolve_execroot_path(p) for p in _env_val.split(":")
- )
+ _dot_path = Path(_resolve_execroot_path(os.environ["GRAPHVIZ_DOT"]))
+ # Derive library search paths from the binary location so the rule passes
+ # only GRAPHVIZ_DOT and conf.py stays self-contained.
+ # The graphviz cmake deb installs:
+ # usr/bin/dot_builtins ← GRAPHVIZ_DOT points here
+ # usr/lib/*.so* ← LD_LIBRARY_PATH (core shared libs)
+ # usr/lib/graphviz/*.so* ← LTDL_LIBRARY_PATH (layout/render plugins)
+ _usr_dir = _dot_path.parent.parent # usr/bin → parent → usr
+ os.environ["LD_LIBRARY_PATH"] = str(_usr_dir / "lib")
+ os.environ["LTDL_LIBRARY_PATH"] = str(_usr_dir / "lib" / "graphviz")
+ plantuml = f"{plantuml_path} -graphvizdot {_dot_path}"
else:
- graphviz_dot = "/__hermetic_graphviz_not_configured__/dot"
+ logger.warning(
+ "GRAPHVIZ_DOT not set; PlantUML falling back to Smetana layout engine. "
+ "Hermetic dot (@graphviz_deb) is only available on linux_x86_64."
+ )
+ plantuml = f"{plantuml_path} -Playout=smetana"
+plantuml_output_format = "svg_obj"
# HTML theme
html_theme = "sphinx_rtd_theme"
diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD
index d4b879cb..f797ef31 100644
--- a/bazel/rules/rules_score/test/BUILD
+++ b/bazel/rules/rules_score/test/BUILD
@@ -126,12 +126,13 @@ sphinx_module(
],
)
-# Test 2: Graphviz Rendering
-# Tests hermetic graphviz support via sphinx.ext.graphviz directive
+# Test 2: PlantUML dot rendering
+# Tests that the hermetic dot binary is wired to PlantUML via -graphvizdot
+# and that @startdot content in a .. uml:: directive renders to SVG.
sphinx_module(
- name = "graphviz_test_lib",
- srcs = glob(["fixtures/graphviz_test/*.rst"]),
- index = "fixtures/graphviz_test/index.rst",
+ name = "plantuml_dot_test_lib",
+ srcs = glob(["fixtures/plantuml_dot_test/*.rst"]),
+ index = "fixtures/plantuml_dot_test/index.rst",
sphinx = "@score_tooling//bazel/rules/rules_score:score_build",
)
@@ -788,11 +789,11 @@ py_test(
)
py_test(
- name = "test_graphviz_rendering",
+ name = "test_plantuml_dot_rendering",
size = "small",
- srcs = ["graphviz_render_test.py"],
- data = [":graphviz_test_lib"],
- main = "graphviz_render_test.py",
+ srcs = ["plantuml_dot_render_test.py"],
+ data = [":plantuml_dot_test_lib"],
+ main = "plantuml_dot_render_test.py",
)
# Combined test suite for all tests
@@ -805,6 +806,7 @@ test_suite(
":sphinx_module_tests",
":test_aou_forwarding_to_lobster",
":test_graphviz_rendering",
+ ":test_plantuml_dot_rendering",
":test_rst_to_trlc",
":test_safety_analysis_tools",
":test_trlc_rst_image_rendering",
diff --git a/bazel/rules/rules_score/test/fixtures/graphviz_test/index.rst b/bazel/rules/rules_score/test/fixtures/plantuml_dot_test/index.rst
similarity index 57%
rename from bazel/rules/rules_score/test/fixtures/graphviz_test/index.rst
rename to bazel/rules/rules_score/test/fixtures/plantuml_dot_test/index.rst
index cb44cddb..f721b2fd 100644
--- a/bazel/rules/rules_score/test/fixtures/graphviz_test/index.rst
+++ b/bazel/rules/rules_score/test/fixtures/plantuml_dot_test/index.rst
@@ -1,6 +1,6 @@
..
# *******************************************************************************
- # Copyright (c) 2026 Contributors to the Eclipse Foundation
+ # Copyright (c) 2025 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
@@ -12,18 +12,19 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
-Graphviz Rendering Test
-=======================
+PlantUML dot Rendering Test
+===========================
-This document tests the ``sphinx.ext.graphviz`` directive to ensure hermetic graphviz
-integration is working correctly.
+This document verifies that the hermetic ``dot_builtins`` binary is correctly
+wired to PlantUML via ``-graphvizdot`` and that ``@startdot`` content inside a
+``.. uml::`` directive is rendered as an SVG image.
-Simple DAG
-----------
+Simple DAG via @startdot
+------------------------
-.. graphviz::
- :align: center
+.. uml::
+ @startdot
digraph {
A -> B;
A -> C;
@@ -31,5 +32,6 @@ Simple DAG
C -> D;
label = "Simple DAG";
}
+ @enddot
-This graphviz diagram should render as SVG in the produced HTML output.
+The diagram above should appear as an SVG in the produced HTML output.
diff --git a/bazel/rules/rules_score/test/graphviz_render_test.py b/bazel/rules/rules_score/test/graphviz_render_test.py
deleted file mode 100644
index 8a92641c..00000000
--- a/bazel/rules/rules_score/test/graphviz_render_test.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# *******************************************************************************
-# Copyright (c) 2025 Contributors to the Eclipse Foundation
-#
-# See the NOTICE file(s) distributed with this work for additional
-# information regarding copyright ownership.
-#
-# This program and the accompanying materials are made available under the
-# terms of the Apache License Version 2.0 which is available at
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# SPDX-License-Identifier: Apache-2.0
-# *******************************************************************************
-"""Integration test for hermetic graphviz rendering in sphinx_module.
-
-Verifies that the sphinx.ext.graphviz extension successfully renders diagrams
-to SVG when using hermetic graphviz bundled via download_deb.
-"""
-
-import os
-import unittest
-
-
-def _runfile(*parts: str) -> str:
- """Locate a file in the Bazel runfiles tree."""
- srcdir = os.environ["TEST_SRCDIR"]
- workspace = os.environ.get("TEST_WORKSPACE", "_main")
- for candidate in [
- os.path.join(srcdir, workspace, *parts),
- os.path.join(srcdir, *parts),
- ]:
- if os.path.exists(candidate):
- return candidate
- raise FileNotFoundError(
- f"Runfile not found: {os.path.join(*parts)}\n"
- f" Searched under TEST_SRCDIR={srcdir}"
- )
-
-
-def _find_generated_html_root() -> str:
- """Locate the generated HTML directory for graphviz_test_lib in runfiles.
-
- Depending on Bazel/runfiles layout, the directory artifact may appear either
- at ``.../graphviz_test_lib/html`` or at ``.../graphviz_test_lib``.
- """
- for parts in [
- ("bazel/rules/rules_score/test/graphviz_test_lib/html",),
- ("bazel/rules/rules_score/test/graphviz_test_lib",),
- ]:
- try:
- root = _runfile(*parts)
- except FileNotFoundError:
- continue
- if os.path.exists(os.path.join(root, "index.html")):
- return root
-
- raise FileNotFoundError(
- "Unable to locate generated graphviz_test_lib HTML output in runfiles"
- )
-
-
-class TestGraphvizRendering(unittest.TestCase):
- """Verify that sphinx.ext.graphviz renders an SVG artifact."""
-
- def test_graphviz_renders_svg(self):
- """Test that graphviz output is rendered as SVG in generated HTML."""
- html_dir = _find_generated_html_root()
-
- svg_files = [
- file_name
- for _, _, files in os.walk(html_dir)
- for file_name in files
- if file_name.endswith(".svg")
- ]
-
- self.assertTrue(
- svg_files,
- "Generated HTML output should include at least one rendered graphviz SVG artifact",
- )
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/bazel/rules/rules_score/test/plantuml_dot_render_test.py b/bazel/rules/rules_score/test/plantuml_dot_render_test.py
new file mode 100644
index 00000000..40b2e722
--- /dev/null
+++ b/bazel/rules/rules_score/test/plantuml_dot_render_test.py
@@ -0,0 +1,91 @@
+# *******************************************************************************
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+"""Integration test for hermetic dot wiring via PlantUML in sphinx_module.
+
+Verifies that a ``.. uml:: @startdot`` block in an RST source file is rendered
+to an SVG image when the hermetic dot binary is wired to PlantUML via
+``-graphvizdot``. The test inspects the generated HTML for an ``
`` tag
+whose ``src`` points at a ``.svg`` file whose content contains nodes from the
+DOT graph (confirming that dot actually ran, not just that any SVG exists).
+"""
+
+import os
+import unittest
+
+
+def _runfile(*parts: str) -> str:
+ """Locate a file in the Bazel runfiles tree."""
+ srcdir = os.environ["TEST_SRCDIR"]
+ workspace = os.environ.get("TEST_WORKSPACE", "_main")
+ for candidate in [
+ os.path.join(srcdir, workspace, *parts),
+ os.path.join(srcdir, *parts),
+ ]:
+ if os.path.exists(candidate):
+ return candidate
+ raise FileNotFoundError(
+ f"Runfile not found: {os.path.join(*parts)}\n"
+ f" Searched under TEST_SRCDIR={srcdir}"
+ )
+
+
+def _find_html_root() -> str:
+ """Locate the generated HTML directory for plantuml_dot_test_lib."""
+ path = _runfile("bazel/rules/rules_score/test/plantuml_dot_test_lib/html")
+ if os.path.isfile(os.path.join(path, "index.html")):
+ return path
+ raise FileNotFoundError(
+ "Unable to locate generated plantuml_dot_test_lib HTML output in runfiles"
+ )
+
+
+class TestPlantUMLDotRendering(unittest.TestCase):
+ """Verify that @startdot content in .. uml:: is rendered to SVG via PlantUML."""
+
+ def test_dot_graph_renders_to_svg(self):
+ """An SVG image file must be produced from the @startdot directive.
+
+ sphinxcontrib-plantuml may write the SVG to ``_images/``, ``_plantuml/``,
+ or both depending on the version. We check all directories except
+ ``_static/`` (which only contains theme assets) and require at least one
+ non-trivial SVG (>100 bytes) to be present, confirming that PlantUML
+ processed the ``@startdot`` block and produced a rendered image rather
+ than an error placeholder or nothing at all.
+ """
+ html_dir = _find_html_root()
+
+ svg_files = [
+ os.path.join(dirpath, fname)
+ for dirpath, _, fnames in os.walk(html_dir)
+ for fname in fnames
+ if fname.endswith(".svg") and "_static" not in dirpath
+ ]
+
+ self.assertTrue(
+ svg_files,
+ f"Expected at least one rendered SVG outside _static/ in the generated "
+ f"HTML output (PlantUML @startdot should produce an SVG image).\n"
+ f"HTML root: {html_dir}\n"
+ f"All files: {[os.path.relpath(os.path.join(d, f), html_dir) for d, _, fs in os.walk(html_dir) for f in fs]}",
+ )
+
+ non_trivial = [f for f in svg_files if os.path.getsize(f) > 100]
+ self.assertTrue(
+ non_trivial,
+ f"SVG files found but all are trivially small (<= 100 bytes): {svg_files}\n"
+ "PlantUML may have produced an error placeholder instead of the rendered graph.",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/lobster_rst_report/BUILD b/tools/lobster_rst_report/BUILD
index 8bfe6368..cdec3bc9 100644
--- a/tools/lobster_rst_report/BUILD
+++ b/tools/lobster_rst_report/BUILD
@@ -19,7 +19,6 @@ py_library(
"__init__.py",
"_helpers.py",
"_renderers.py",
- "graphviz_utils.py",
"rst_report.py",
],
imports = [".."],
diff --git a/tools/lobster_rst_report/_helpers.py b/tools/lobster_rst_report/_helpers.py
index 59085012..b8d19c08 100644
--- a/tools/lobster_rst_report/_helpers.py
+++ b/tools/lobster_rst_report/_helpers.py
@@ -19,13 +19,11 @@
* :class:`RstUtils` -- RST text escaping and heading generation
* :class:`ItemNaming` -- label, page-name, and kind-string derivation
* :class:`TracingClassifier` -- message classification and status-to-CSS mapping
-* :class:`PolicyDiagramBuilder`-- Graphviz DOT generation for the tracing policy
+* :class:`PolicyDiagramBuilder`-- PlantUML @startdot diagram for the tracing policy
"""
from typing import Dict, Tuple
-import os
-
from lobster.common.report import Report
from lobster.common.location import (
Void_Reference,
@@ -34,7 +32,6 @@
Codebeamer_Reference,
)
from lobster.common.items import Item, Requirement, Implementation, Activity
-from .graphviz_utils import is_dot_available
class RstUtils:
@@ -313,7 +310,7 @@ def card_header_class(cls, status_name: str) -> str:
class PolicyDiagramBuilder:
- """Build a Graphviz DOT diagram representing the configured tracing policy.
+ """Build a PlantUML @startdot diagram representing the configured tracing policy.
Nodes represent tracing levels, coloured by kind:
@@ -323,6 +320,11 @@ class PolicyDiagramBuilder:
Directed edges follow the ``traces`` relationship (level A → level B means
A must be traceable to B).
+
+ The diagram is emitted as a ``.. uml::`` directive with an inline
+ ``@startdot ... @enddot`` block so ``sphinxcontrib.plantuml`` renders it
+ via the hermetic ``dot`` binary (or Smetana fallback) without requiring
+ ``sphinx.ext.graphviz``.
"""
#: Fill and font colour pairs keyed by level kind.
@@ -371,11 +373,10 @@ def _build_dot_lines(cls, report: Report) -> list:
def build(cls, report: Report, indent: int = 0) -> list:
"""Return RST lines for the tracing-policy diagram.
- When Graphviz ``dot`` is available, emits a ``.. graphviz::`` directive
- that Sphinx renders as an image. When ``dot`` is not found, emits a
- ``.. note::`` explaining how to install Graphviz together with the raw
- DOT source in a ``.. code-block::`` so the diagram can still be
- visualised manually.
+ Emits a ``.. uml::`` directive with an inline ``@startdot ... @enddot``
+ block. ``sphinxcontrib.plantuml`` passes the body to PlantUML which
+ renders it via the hermetic ``dot`` binary (or Smetana on platforms
+ without native dot).
Args:
report: The loaded LOBSTER report whose ``config`` provides level
@@ -392,32 +393,12 @@ def build(cls, report: Report, indent: int = 0) -> list:
nested_indent = indent_str + " "
dot_lines = cls._build_dot_lines(report)
- # Respect an explicit GRAPHVIZ_DOT env var set by the build system
- # (e.g. via --action_env=GRAPHVIZ_DOT=/usr/bin/dot in CI).
- dot_bin = os.environ.get("GRAPHVIZ_DOT") or None
- if is_dot_available(dot_bin):
- out = []
- out.append(f"{indent_str}.. graphviz::")
- out.append("")
- for dot_line in dot_lines:
- out.append(f"{nested_indent}{dot_line}")
- out.append("")
- return out
out = []
- out.append(f"{indent_str}.. note::")
- out.append("")
- out.append(
- f"{nested_indent}The tracing-policy diagram below could not be rendered "
- f"because the Graphviz ``dot`` utility was not found."
- )
- out.append(
- f"{nested_indent}Install `Graphviz `__ and rebuild "
- f"to see the diagram as an image."
- )
- out.append("")
- out.append(f"{nested_indent}.. code-block:: dot")
+ out.append(f"{indent_str}.. uml::")
out.append("")
+ out.append(f"{nested_indent}@startdot")
for dot_line in dot_lines:
- out.append(f"{nested_indent} {dot_line}")
+ out.append(f"{nested_indent}{dot_line}")
+ out.append(f"{nested_indent}@enddot")
out.append("")
return out
diff --git a/tools/lobster_rst_report/graphviz_utils.py b/tools/lobster_rst_report/graphviz_utils.py
deleted file mode 100644
index 352dd428..00000000
--- a/tools/lobster_rst_report/graphviz_utils.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# *******************************************************************************
-# Copyright (c) 2026 Contributors to the Eclipse Foundation
-#
-# See the NOTICE file(s) distributed with this work for additional
-# information regarding copyright ownership.
-#
-# This program and the accompanying materials are made available under the
-# terms of the Apache License Version 2.0 which is available at
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# SPDX-License-Identifier: Apache-2.0
-# *******************************************************************************
-
-"""Shared Graphviz utilities for LOBSTER report tools."""
-
-import subprocess
-from typing import Optional
-
-
-def is_dot_available(dot: Optional[str] = None) -> bool:
- """Return True if the ``dot`` executable (Graphviz) is on PATH.
-
- Args:
- dot: Optional explicit path to the ``dot`` binary. When ``None``
- (default) the system PATH is searched.
-
- Returns:
- ``True`` if Graphviz ``dot`` is available, ``False`` otherwise.
- """
- try:
- subprocess.run(
- [dot if dot else "dot", "-V"],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- encoding="UTF-8",
- check=True,
- timeout=5,
- )
- return True
- except (
- FileNotFoundError,
- subprocess.TimeoutExpired,
- subprocess.CalledProcessError,
- ):
- return False
diff --git a/tools/lobster_rst_report/rst_report.py b/tools/lobster_rst_report/rst_report.py
index 309c3ae3..629f6d4d 100644
--- a/tools/lobster_rst_report/rst_report.py
+++ b/tools/lobster_rst_report/rst_report.py
@@ -40,8 +40,6 @@
from lobster.common.meta_data_tool_base import MetaDataToolBase
from lobster.common.exceptions import LOBSTER_Exception
from lobster.common.errors import LOBSTER_Error
-from .graphviz_utils import is_dot_available
-
from ._helpers import RstUtils, ItemNaming, PolicyDiagramBuilder
from ._renderers import (
_KIND_ORDER,
@@ -362,13 +360,6 @@ def _run_impl(self, options: argparse.Namespace) -> int:
err.dump()
return 1
- if not is_dot_available():
- print(
- "warning: dot utility not found, report will not include "
- "the tracing policy visualisation"
- )
- print("> please install Graphviz (https://graphviz.org)")
-
# lobster-trace: UseCases.RST_Output
# lobster-trace: rst_req.RST_Report_Multi_Page
# lobster-trace: rst_req.Valid_Lobster_File_Multi_Page