Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
)
Expand Down
5 changes: 5 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 31 additions & 0 deletions bazel/rules/rules_score/private/sphinx_module.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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"],
)
Expand Down
68 changes: 66 additions & 2 deletions bazel/rules/rules_score/templates/conf.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import json
import os
import shutil as _shutil
import sys
from pathlib import Path
from typing import Any, Dict, List
Expand All @@ -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))
Expand All @@ -55,6 +98,7 @@
"sphinxcontrib.plantuml",
"trlc",
"clickable_plantuml",
"sphinx.ext.graphviz",
]

# MyST parser extensions
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions patches/BUILD
Original file line number Diff line number Diff line change
@@ -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"])
11 changes: 11 additions & 0 deletions patches/download_utils_add_gz_support.patch
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 17 additions & 0 deletions third_party/graphviz/BUILD
Original file line number Diff line number Diff line change
@@ -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.
101 changes: 101 additions & 0 deletions third_party/graphviz/graphviz.BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
1 change: 0 additions & 1 deletion tools/sphinx/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Loading