Skip to content

Commit 352f7f1

Browse files
committed
Added hermetic Graphviz support for Sphinx
Optimize conf template path/env resolution
1 parent e521641 commit 352f7f1

7 files changed

Lines changed: 298 additions & 3 deletions

File tree

MODULE.bazel

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,21 @@ deb(
254254
urls = ["https://archive.ubuntu.com/ubuntu/pool/universe/l/lcov/lcov_2.0-4ubuntu2_all.deb"],
255255
)
256256

257+
###############################################################################
258+
# Graphviz deb package (cmake release; bundles all graphviz .so files so
259+
# dot_builtins runs without system graphviz installation)
260+
# Uses custom repository rule because the cmake deb uses data.tar.gz
261+
# which download_utils doesn't support (only .xz and .zst)
262+
###############################################################################
263+
graphviz_deb_rule = use_repo_rule("//third_party/graphviz:defs.bzl", "graphviz_deb")
264+
265+
graphviz_deb_rule(
266+
name = "graphviz_deb",
267+
build = "//third_party/graphviz:graphviz.BUILD",
268+
integrity = "sha256-Jk5gSqo8l0INoY+kr1ZAsi2WhZY8LlAFlEag54H3Q2Q=",
269+
urls = ["https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/12.2.1/ubuntu_24.04_graphviz-12.2.1-cmake.deb"],
270+
)
271+
257272
register_toolchains(
258273
"//bazel/rules/rules_score:sphinx_default_toolchain",
259274
)

bazel/rules/rules_score/private/sphinx_module.bzl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,33 @@ def _score_html_impl(ctx):
223223
"--log-level",
224224
get_log_level(ctx),
225225
]
226+
227+
# Wire in the hermetic graphviz deb (dot_builtins + bundled shared libs).
228+
# conf.template.py resolves all three env vars (GRAPHVIZ_DOT,
229+
# LD_LIBRARY_PATH, LTDL_LIBRARY_PATH) from execroot-relative to absolute
230+
# paths so dot_builtins can load its plugins without a system installation.
231+
graphviz_files = ctx.files.graphviz
232+
dot_binary = None
233+
for f in graphviz_files:
234+
if f.path.endswith("/usr/bin/dot_builtins"):
235+
dot_binary = f
236+
break
237+
if not dot_binary:
238+
fail("graphviz target {} must provide usr/bin/dot_builtins".format(ctx.attr.graphviz.label))
239+
240+
graphviz_prefix = dot_binary.path[:-len("/usr/bin/dot_builtins")]
241+
graphviz_env = {
242+
"GRAPHVIZ_DOT": dot_binary.path,
243+
"LD_LIBRARY_PATH": graphviz_prefix + "/usr/lib",
244+
"LTDL_LIBRARY_PATH": graphviz_prefix + "/usr/lib/graphviz",
245+
}
246+
html_inputs = html_inputs + graphviz_files
247+
226248
ctx.actions.run(
227249
inputs = html_inputs,
228250
outputs = [sphinx_html_output],
229251
arguments = html_args + [args],
252+
env = graphviz_env,
230253
progress_message = "Building HTML: %s" % ctx.label.name,
231254
executable = sphinx_toolchain.sphinx.files_to_run.executable,
232255
tools = [
@@ -312,6 +335,12 @@ _score_html = rule(
312335
extra_opts = attr.string_list(
313336
doc = "Regular additional string options to pass onto Sphinx.",
314337
),
338+
graphviz = attr.label(
339+
default = Label("@graphviz_deb//:all"),
340+
allow_files = True,
341+
doc = "Graphviz cmake-release deb files (dot_builtins binary + bundled libs). " +
342+
"Provides a hermetic 'dot' binary without requiring a system graphviz installation.",
343+
),
315344
),
316345
toolchains = ["//bazel/rules/rules_score:toolchain_type"],
317346
)

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

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import json
2222
import os
23+
import shutil as _shutil
2324
import sys
2425
from pathlib import Path
2526
from typing import Any, Dict, List
@@ -30,6 +31,48 @@
3031
# Create a logger with the Sphinx namespace
3132
logger = logging.getLogger(__name__)
3233

34+
# ---------------------------------------------------------------------------
35+
# Helpers: Bazel execroot path resolution
36+
# ---------------------------------------------------------------------------
37+
38+
39+
def _bazel_execroot() -> Path:
40+
"""Return the Bazel execroot directory inferred from this config file's path.
41+
42+
conf.py is generated into ``bazel-out/…/bin/…/conf.py``, so splitting on
43+
``/bazel-out/`` gives us the execroot prefix reliably. Falls back to the
44+
current working directory when the path pattern is not recognised (e.g.
45+
during unit tests or IDE runs outside Bazel).
46+
"""
47+
parts = str(Path(__file__).resolve()).split("/bazel-out/", 1)
48+
return Path(parts[0]) if len(parts) == 2 else Path.cwd()
49+
50+
51+
# Computed once at import time so _resolve_execroot_path() doesn't repeat the
52+
# filesystem resolution on every call.
53+
_EXECROOT = _bazel_execroot()
54+
55+
56+
def _resolve_execroot_path(path_value: str) -> str:
57+
"""Resolve an execroot-relative path to an absolute filesystem path.
58+
59+
Bazel passes action inputs as paths relative to the execroot (e.g.
60+
``external/+_repo_rules2+graphviz_deb/usr/bin/dot_builtins``). Those
61+
paths are only valid when the process' cwd is the execroot — which is
62+
not guaranteed once Sphinx changes directories during the build.
63+
64+
This function makes them absolute so they work regardless of cwd.
65+
Absolute paths and plain command names (e.g. ``dot``) are returned
66+
unchanged.
67+
"""
68+
p = Path(path_value)
69+
if p.is_absolute():
70+
return str(p)
71+
if path_value.startswith("external/") or path_value.startswith("bazel-out/"):
72+
return str((_EXECROOT / p).resolve())
73+
return path_value
74+
75+
3376
logger.debug("#" * 80)
3477
logger.debug("# READING CONF.PY")
3578
logger.debug("SYSPATH:" + str(sys.path))
@@ -55,6 +98,7 @@
5598
"sphinxcontrib.plantuml",
5699
"trlc",
57100
"clickable_plantuml",
101+
"sphinx.ext.graphviz",
58102
]
59103

60104
# MyST parser extensions
@@ -153,9 +197,26 @@
153197
plantuml = f"{plantuml_path} -Playout=smetana"
154198
plantuml_output_format = "svg_obj"
155199

156-
import shutil as _shutil
200+
# ---------------------------------------------------------------------------
201+
# Graphviz (sphinx.ext.graphviz)
202+
# ---------------------------------------------------------------------------
203+
# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the hermetic
204+
# dot_builtins binary from @graphviz_deb. The path is execroot-relative, so
205+
# we resolve it to an absolute path here so it remains valid after any cwd
206+
# change that Sphinx may perform during the build.
207+
graphviz_dot = _resolve_execroot_path(
208+
os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot"
209+
)
157210

158-
graphviz_dot = os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot"
211+
# LD_LIBRARY_PATH and LTDL_LIBRARY_PATH are set by the Bazel rule as
212+
# execroot-relative paths. Resolve each component to absolute so they
213+
# remain valid if Sphinx changes cwd before spawning the dot subprocess.
214+
for _env_var in ("LD_LIBRARY_PATH", "LTDL_LIBRARY_PATH"):
215+
_env_val = os.environ.get(_env_var, "")
216+
if _env_val:
217+
os.environ[_env_var] = ":".join(
218+
_resolve_execroot_path(p) for p in _env_val.split(":")
219+
)
159220

160221
# HTML theme
161222
html_theme = "sphinx_rtd_theme"

third_party/graphviz/BUILD

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# This package hosts the BUILD file used by the @graphviz_deb external repository.
15+
# The graphviz_deb rule (defined in MODULE.bazel) extracts the Graphviz cmake
16+
# release .deb and uses graphviz.BUILD as its top-level BUILD file.

third_party/graphviz/defs.bzl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
"""Repository rule that downloads and extracts a graphviz cmake-release .deb.
15+
16+
The upstream cmake deb uses data.tar.gz compression, which download_utils does
17+
not support (it only handles .xz and .zst). This rule uses the standard
18+
`ar` + `tar` toolchain that is present on every Debian/Ubuntu host.
19+
"""
20+
21+
def _graphviz_deb_impl(ctx):
22+
"""Download and extract a graphviz .deb package into an external repository."""
23+
24+
# Step 1: download the .deb archive.
25+
deb_path = ctx.path("graphviz.deb")
26+
ctx.download(
27+
url = ctx.attr.urls,
28+
integrity = ctx.attr.integrity,
29+
output = deb_path,
30+
)
31+
32+
work_dir = str(ctx.path("."))
33+
34+
# Step 2: unpack data.tar.gz from the ar archive.
35+
# A Debian .deb is an ar archive containing control.tar.* and data.tar.*.
36+
result = ctx.execute(
37+
["ar", "x", str(deb_path), "data.tar.gz"],
38+
working_directory = work_dir,
39+
)
40+
if result.return_code != 0:
41+
fail("Failed to extract data.tar.gz from deb: {}".format(result.stderr))
42+
43+
# Step 3: extract data.tar.gz contents into the repository root.
44+
result = ctx.execute(
45+
["tar", "-xzf", "data.tar.gz"],
46+
working_directory = work_dir,
47+
)
48+
if result.return_code != 0:
49+
fail("Failed to extract data.tar.gz: {}".format(result.stderr))
50+
51+
# Clean up only the files we explicitly created.
52+
ctx.execute(["rm", "-f", str(deb_path), "data.tar.gz"], working_directory = work_dir)
53+
54+
# Step 4: inject the BUILD file that exposes graphviz targets.
55+
ctx.file("BUILD", ctx.read(ctx.attr.build))
56+
57+
graphviz_deb = repository_rule(
58+
doc = "Downloads and extracts a graphviz cmake-release .deb into an external repository.",
59+
implementation = _graphviz_deb_impl,
60+
attrs = {
61+
"urls": attr.string_list(
62+
mandatory = True,
63+
doc = "List of mirror URLs for the graphviz .deb package.",
64+
),
65+
"integrity": attr.string(
66+
mandatory = True,
67+
doc = "Subresource Integrity (SRI) checksum of the .deb archive.",
68+
),
69+
"build": attr.label(
70+
mandatory = True,
71+
doc = "Label of the BUILD file template to inject into the extracted repository.",
72+
),
73+
},
74+
)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# This BUILD file is injected into the @graphviz_deb external repository by the
15+
# download_deb rule. It exposes the cmake-built graphviz binaries and their
16+
# bundled shared libraries.
17+
#
18+
# == Use case ==
19+
# We use graphviz exclusively to render the LOBSTER tracing-policy diagram as
20+
# SVG inside Sphinx (sphinx.ext.graphviz, -Tsvg, dot layout algorithm).
21+
#
22+
# == Plugins activated for our use case ==
23+
# Only two of the bundled plugins are activated at runtime for -Tsvg + dot layout:
24+
# libgvplugin_core.so.6 — SVG/PS/JSON renderer ("render: svg:core")
25+
# libgvplugin_dot_layout.so.6 — Hierarchical "dot" layout algorithm
26+
#
27+
# All other plugins in usr/lib/graphviz/ (pango, gd, neato_layout, vt, …) are
28+
# registered at startup from the config6 file and then loaded on demand.
29+
# For our -Tsvg + dot-layout use case they are never invoked; if their system
30+
# dependencies are absent, graphviz emits a warning but SVG output is unaffected.
31+
#
32+
# == System library dependencies ==
33+
# The cmake deb bundles all graphviz-specific .so files so that `dot_builtins`
34+
# finds them via RUNPATH=$ORIGIN/../lib without a system graphviz installation.
35+
# The remaining system libraries are split by whether they are required for our
36+
# specific use case or only pulled in by unused plugins:
37+
#
38+
# Required by libgvplugin_core + libgvplugin_dot_layout (our actual use case):
39+
# libc.so.6 — C standard library (always present)
40+
# libm.so.6 — math library (always present)
41+
# libz.so.1 — zlib compression (always present: zlib1g)
42+
# libexpat.so.1 — XML/SVG parsing (always present: libexpat1)
43+
# libltdl.so.7 — plugin dynamic loader (libtool) (always present: libltdl7)
44+
#
45+
# Only required by unused plugins (pango/gd/neato_layout) — NOT needed for SVG:
46+
# libcairo.so.2 + libpixman-1.so.0 + libxcb*.so — raster/PDF rendering
47+
# (pango+gd plugins; pre-installed on Ubuntu 24.04)
48+
# libpango*.so + libfontconfig.so.1 + libfreetype.so.6
49+
# + libharfbuzz.so.0 + libfribidi.so.0 + libthai.so.0 + libdatrie.so.1
50+
# + libgraphite2.so.3 — font layout for PNG/PDF output
51+
# (pango plugin; pre-installed on Ubuntu 24.04)
52+
# libgd.so.3 + libjpeg.so.8 + libpng16.so.16 + libtiff.so.6 + libwebp.so.7
53+
# + libheif.so.1 + libLerc.so.4 + libjbig.so.0 + libdeflate.so.0
54+
# + libbrotli*.so + libzstd.so.1 + liblzma.so.5 + libsharpyuv.so.0
55+
# — image-format decoders for PNG/GIF/JPEG output
56+
# (gd plugin; pre-installed on Ubuntu 24.04)
57+
# libgts-0.7.so.5 — graph triangulation
58+
# (neato_layout only; NOT needed for dot layout)
59+
# libglib-2.0.so.0 + libgio-2.0.so.0 + libgobject-2.0.so.0
60+
# + libgmodule-2.0.so.0 + libffi.so.8 + libpcre2-8.so.0
61+
# + libblkid.so.1 + libmount.so.1 + libselinux.so.1
62+
# + libbsd.so.0 + libmd.so.0 — GLib/GIO stack (pango plugin transitive deps)
63+
# (pre-installed on Ubuntu 24.04)
64+
# libX11.so.6 + libXext.so.6 + libXrender.so.1 + libXpm.so.4
65+
# + libXau.so.6 + libXdmcp.so.6 — X11 display (xlib/x11 output only)
66+
# (pre-installed on Ubuntu 24.04)
67+
# libstdc++.so.6 + libgcc_s.so.1 — C++ runtime (gd plugin)
68+
# (always present)
69+
70+
package(default_visibility = ["//visibility:public"])
71+
72+
# The actual graphviz rendering binary (not the dot wrapper/launcher).
73+
# Uses RUNPATH $ORIGIN/../lib to find bundled shared libraries.
74+
filegroup(
75+
name = "dot_binary",
76+
srcs = ["usr/bin/dot_builtins"],
77+
)
78+
79+
# Bundled graphviz shared libraries (libgvc, libcgraph, libcdt, libpathplan, libxdot).
80+
# These are found automatically by dot_builtins via RUNPATH $ORIGIN/../lib.
81+
filegroup(
82+
name = "core_libs",
83+
srcs = glob(["usr/lib/*.so*"]),
84+
)
85+
86+
# Graphviz plugin shared libraries (libgvplugin_core, libgvplugin_dot_layout, etc.).
87+
# Loaded at runtime via libltdl; requires LTDL_LIBRARY_PATH=usr/lib/graphviz.
88+
filegroup(
89+
name = "plugin_libs",
90+
srcs = glob(["usr/lib/graphviz/*.so*"]),
91+
)
92+
93+
# All graphviz files needed to run dot_builtins.
94+
filegroup(
95+
name = "all",
96+
srcs = [
97+
":core_libs",
98+
":dot_binary",
99+
":plugin_libs",
100+
],
101+
)

tools/sphinx/BUILD

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# *******************************************************************************
1313

1414
load("@pip_rules_score//:requirements.bzl", "requirement")
15-
load("@pip_tooling//:requirements.bzl", "requirement")
1615
load("@rules_java//java:defs.bzl", "java_binary")
1716
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary")
1817

0 commit comments

Comments
 (0)