From cc1711a8bf0f2362879f3e1df6402ec1d34a3861 Mon Sep 17 00:00:00 2001 From: ambujsingh Date: Fri, 12 Jun 2026 18:49:41 +0530 Subject: [PATCH] Added hermetic Graphviz support for Sphinx Optimize conf template path/env resolution --- MODULE.bazel | 21 ++++ REUSE.toml | 5 + .../rules_score/private/sphinx_module.bzl | 31 ++++++ .../rules_score/templates/conf.template.py | 68 +++++++++++- patches/BUILD | 16 +++ patches/download_utils_add_gz_support.patch | 11 ++ third_party/graphviz/BUILD | 17 +++ third_party/graphviz/graphviz.BUILD | 101 ++++++++++++++++++ tools/sphinx/BUILD | 1 - 9 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 patches/BUILD create mode 100644 patches/download_utils_add_gz_support.patch create mode 100644 third_party/graphviz/BUILD create mode 100644 third_party/graphviz/graphviz.BUILD diff --git a/MODULE.bazel b/MODULE.bazel index 9c08d4aa..d0ea0538 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -34,6 +34,12 @@ bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2") bazel_dep(name = "flatbuffers", version = "25.9.23") bazel_dep(name = "download_utils", version = "1.2.2") +single_version_override( + module_name = "download_utils", + patch_strip = 1, + patches = ["//patches:download_utils_add_gz_support.patch"], + version = "1.2.2", +) # flatbuffers depends on this transitively, but older grpc-java version # The main problem is that there the command `bazel mod deps` is broken, which @@ -254,6 +260,21 @@ deb( urls = ["https://archive.ubuntu.com/ubuntu/pool/universe/l/lcov/lcov_2.0-4ubuntu2_all.deb"], ) +############################################################################### +# Graphviz deb package (cmake release; bundles all graphviz .so files so +# dot_builtins runs without system graphviz installation) +# Uses download_deb (from download_utils) with a one-line upstream patch that +# adds data.tar.gz support alongside the existing .xz/.zst extraction. +# Patch: patches/download_utils_add_gz_support.patch +# TODO: remove single_version_override once the fix is merged upstream. +############################################################################### +deb( + name = "graphviz_deb", + build = "//third_party/graphviz:graphviz.BUILD", + integrity = "sha256-Jk5gSqo8l0INoY+kr1ZAsi2WhZY8LlAFlEag54H3Q2Q=", + urls = ["https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/12.2.1/ubuntu_24.04_graphviz-12.2.1-cmake.deb"], +) + register_toolchains( "//bazel/rules/rules_score:sphinx_default_toolchain", ) diff --git a/REUSE.toml b/REUSE.toml index 04fb790c..e12a76cf 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -58,6 +58,11 @@ path = ["plantuml/**/*.json"] SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" SPDX-License-Identifier = "Apache-2.0" +[[annotations]] +path = ["patches/*.patch"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + [[annotations]] path = ["plantuml/**/*.svg"] SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index 15b4527d..bcca5feb 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -228,10 +228,35 @@ def _score_html_impl(ctx): "--log-level", get_log_level(ctx), ] + + # Wire in the hermetic graphviz deb (dot_builtins + bundled shared libs). + # 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. + _dot_suffix = "/usr/bin/dot_builtins" + graphviz_files = ctx.files.graphviz + 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.label)) + + 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 + ctx.actions.run( inputs = html_inputs, outputs = [sphinx_html_output], arguments = html_args + [args], + env = graphviz_env, + use_default_shell_env = True, progress_message = "Building HTML: %s" % ctx.label.name, executable = sphinx_toolchain.sphinx.files_to_run.executable, tools = [ @@ -323,6 +348,12 @@ _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( + default = Label("@graphviz_deb//:all"), + allow_files = True, + doc = "Graphviz cmake-release deb files (dot_builtins binary + bundled libs). " + + "Provides a hermetic 'dot' binary without requiring a system graphviz installation.", + ), ), toolchains = ["//bazel/rules/rules_score:toolchain_type"], ) diff --git a/bazel/rules/rules_score/templates/conf.template.py b/bazel/rules/rules_score/templates/conf.template.py index 0646a342..93ba9fd1 100644 --- a/bazel/rules/rules_score/templates/conf.template.py +++ b/bazel/rules/rules_score/templates/conf.template.py @@ -20,6 +20,7 @@ import json import os +import shutil as _shutil import sys from pathlib import Path from typing import Any, Dict, List @@ -30,6 +31,48 @@ # Create a logger with the Sphinx namespace logger = logging.getLogger(__name__) +# --------------------------------------------------------------------------- +# Helpers: Bazel execroot path resolution +# --------------------------------------------------------------------------- + + +def _bazel_execroot() -> Path: + """Return the Bazel execroot directory inferred from this config file's path. + + conf.py is generated into ``bazel-out/…/bin/…/conf.py``, so splitting on + ``/bazel-out/`` gives us the execroot prefix reliably. Falls back to the + current working directory when the path pattern is not recognised (e.g. + during unit tests or IDE runs outside Bazel). + """ + parts = str(Path(__file__).resolve()).split("/bazel-out/", 1) + return Path(parts[0]) if len(parts) == 2 else Path.cwd() + + +# Computed once at import time so _resolve_execroot_path() doesn't repeat the +# filesystem resolution on every call. +_EXECROOT = _bazel_execroot() + + +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. + + This function makes them absolute so they work regardless of cwd. + Absolute paths and plain command names (e.g. ``dot``) are returned + unchanged. + """ + p = Path(path_value) + if p.is_absolute(): + return str(p) + if path_value.startswith("external/") or path_value.startswith("bazel-out/"): + return str((_EXECROOT / p).resolve()) + return path_value + + logger.debug("#" * 80) logger.debug("# READING CONF.PY") logger.debug("SYSPATH:" + str(sys.path)) @@ -55,6 +98,7 @@ "sphinxcontrib.plantuml", "trlc", "clickable_plantuml", + "sphinx.ext.graphviz", ] # MyST parser extensions @@ -153,9 +197,29 @@ plantuml = f"{plantuml_path} -Playout=smetana" plantuml_output_format = "svg_obj" -import shutil as _shutil +# --------------------------------------------------------------------------- +# Graphviz (sphinx.ext.graphviz) +# --------------------------------------------------------------------------- +# 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. +graphviz_dot = _resolve_execroot_path( + os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot" +) -graphviz_dot = os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot" +# 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(":") + ) # HTML theme html_theme = "sphinx_rtd_theme" diff --git a/patches/BUILD b/patches/BUILD new file mode 100644 index 00000000..41736333 --- /dev/null +++ b/patches/BUILD @@ -0,0 +1,16 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# Patch files applied to upstream Bazel module dependencies via single_version_override. + +exports_files(["download_utils_add_gz_support.patch"]) diff --git a/patches/download_utils_add_gz_support.patch b/patches/download_utils_add_gz_support.patch new file mode 100644 index 00000000..5542bae8 --- /dev/null +++ b/patches/download_utils_add_gz_support.patch @@ -0,0 +1,11 @@ +--- a/download/deb/repository.bzl ++++ b/download/deb/repository.bzl +@@ -67,7 +67,7 @@ + def implementation(rctx): + canonical = {a: getattr(rctx.attr, a) for a in ATTRS} | {"name": rctx.name} + +- canonical |= download_and_extract(rctx, nested = ("data.tar.xz", "data.tar.zst"), extension = ".deb") ++ canonical |= download_and_extract(rctx, nested = ("data.tar.xz", "data.tar.zst", "data.tar.gz"), extension = ".deb") + canonical |= build(rctx) + canonical |= patch(rctx) + canonical |= links(rctx) diff --git a/third_party/graphviz/BUILD b/third_party/graphviz/BUILD new file mode 100644 index 00000000..29d77545 --- /dev/null +++ b/third_party/graphviz/BUILD @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# This package hosts the BUILD file used by the @graphviz_deb external repository. +# The download_deb rule (from @download_utils, patched via +# patches/download_utils_add_gz_support.patch) extracts the Graphviz cmake +# release .deb and uses graphviz.BUILD as its top-level BUILD file. diff --git a/third_party/graphviz/graphviz.BUILD b/third_party/graphviz/graphviz.BUILD new file mode 100644 index 00000000..b65a14a7 --- /dev/null +++ b/third_party/graphviz/graphviz.BUILD @@ -0,0 +1,101 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# This BUILD file is injected into the @graphviz_deb external repository by the +# graphviz_deb rule. It exposes the cmake-built graphviz binaries and their +# bundled shared libraries. +# +# == Use case == +# We use graphviz exclusively to render the LOBSTER tracing-policy diagram as +# SVG inside Sphinx (sphinx.ext.graphviz, -Tsvg, dot layout algorithm). +# +# == Plugins activated for our use case == +# Only two of the bundled plugins are activated at runtime for -Tsvg + dot layout: +# libgvplugin_core.so.6 — SVG/PS/JSON renderer ("render: svg:core") +# libgvplugin_dot_layout.so.6 — Hierarchical "dot" layout algorithm +# +# All other plugins in usr/lib/graphviz/ (pango, gd, neato_layout, vt, …) are +# registered at startup from the config6 file and then loaded on demand. +# For our -Tsvg + dot-layout use case they are never invoked; if their system +# dependencies are absent, graphviz emits a warning but SVG output is unaffected. +# +# == System library dependencies == +# The cmake deb bundles all graphviz-specific .so files so that `dot_builtins` +# finds them via RUNPATH=$ORIGIN/../lib without a system graphviz installation. +# The remaining system libraries are split by whether they are required for our +# specific use case or only pulled in by unused plugins: +# +# Required by libgvplugin_core + libgvplugin_dot_layout (our actual use case): +# libc.so.6 — C standard library (always present) +# libm.so.6 — math library (always present) +# libz.so.1 — zlib compression (always present: zlib1g) +# libexpat.so.1 — XML/SVG parsing (always present: libexpat1) +# libltdl.so.7 — plugin dynamic loader (libtool) (always present: libltdl7) +# +# Only required by unused plugins (pango/gd/neato_layout) — NOT needed for SVG: +# libcairo.so.2 + libpixman-1.so.0 + libxcb*.so — raster/PDF rendering +# (pango+gd plugins; pre-installed on Ubuntu 24.04) +# libpango*.so + libfontconfig.so.1 + libfreetype.so.6 +# + libharfbuzz.so.0 + libfribidi.so.0 + libthai.so.0 + libdatrie.so.1 +# + libgraphite2.so.3 — font layout for PNG/PDF output +# (pango plugin; pre-installed on Ubuntu 24.04) +# libgd.so.3 + libjpeg.so.8 + libpng16.so.16 + libtiff.so.6 + libwebp.so.7 +# + libheif.so.1 + libLerc.so.4 + libjbig.so.0 + libdeflate.so.0 +# + libbrotli*.so + libzstd.so.1 + liblzma.so.5 + libsharpyuv.so.0 +# — image-format decoders for PNG/GIF/JPEG output +# (gd plugin; pre-installed on Ubuntu 24.04) +# libgts-0.7.so.5 — graph triangulation +# (neato_layout only; NOT needed for dot layout) +# libglib-2.0.so.0 + libgio-2.0.so.0 + libgobject-2.0.so.0 +# + libgmodule-2.0.so.0 + libffi.so.8 + libpcre2-8.so.0 +# + libblkid.so.1 + libmount.so.1 + libselinux.so.1 +# + libbsd.so.0 + libmd.so.0 — GLib/GIO stack (pango plugin transitive deps) +# (pre-installed on Ubuntu 24.04) +# libX11.so.6 + libXext.so.6 + libXrender.so.1 + libXpm.so.4 +# + libXau.so.6 + libXdmcp.so.6 — X11 display (xlib/x11 output only) +# (pre-installed on Ubuntu 24.04) +# libstdc++.so.6 + libgcc_s.so.1 — C++ runtime (gd plugin) +# (always present) + +package(default_visibility = ["//visibility:public"]) + +# The actual graphviz rendering binary (not the dot wrapper/launcher). +# Uses RUNPATH $ORIGIN/../lib to find bundled shared libraries. +filegroup( + name = "dot_binary", + srcs = ["usr/bin/dot_builtins"], +) + +# Bundled graphviz shared libraries (libgvc, libcgraph, libcdt, libpathplan, libxdot). +# These are found automatically by dot_builtins via RUNPATH $ORIGIN/../lib. +filegroup( + name = "core_libs", + srcs = glob(["usr/lib/*.so*"]), +) + +# Graphviz plugin shared libraries (libgvplugin_core, libgvplugin_dot_layout, etc.). +# Loaded at runtime via libltdl; requires LTDL_LIBRARY_PATH=usr/lib/graphviz. +filegroup( + name = "plugin_libs", + srcs = glob(["usr/lib/graphviz/*.so*"]), +) + +# All graphviz files needed to run dot_builtins. +filegroup( + name = "all", + srcs = [ + ":core_libs", + ":dot_binary", + ":plugin_libs", + ], +) diff --git a/tools/sphinx/BUILD b/tools/sphinx/BUILD index e0168a64..d1a64d03 100644 --- a/tools/sphinx/BUILD +++ b/tools/sphinx/BUILD @@ -12,7 +12,6 @@ # ******************************************************************************* load("@pip_rules_score//:requirements.bzl", "requirement") -load("@pip_tooling//:requirements.bzl", "requirement") load("@rules_java//java:defs.bzl", "java_binary") load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary")