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