Skip to content

Commit 515baaa

Browse files
committed
Added hermetic Graphviz support for Sphinx
Optimize conf template path/env resolution
1 parent f6546c3 commit 515baaa

6 files changed

Lines changed: 180 additions & 3 deletions

File tree

MODULE.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ bazel_dep(name = "bazel_skylib", version = "1.7.1")
3434
bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2")
3535
bazel_dep(name = "flatbuffers", version = "25.9.23")
3636
bazel_dep(name = "download_utils", version = "1.2.2")
37+
git_override(
38+
module_name = "download_utils",
39+
commit = "3b96912fb6622dda83f25efd1f8ae596fc4a63a6",
40+
remote = "https://gitlab.arm.com/bazel/download_utils.git",
41+
)
3742

3843
# flatbuffers depends on this transitively, but older grpc-java version
3944
# The main problem is that there the command `bazel mod deps` is broken, which
@@ -254,6 +259,19 @@ deb(
254259
urls = ["https://archive.ubuntu.com/ubuntu/pool/universe/l/lcov/lcov_2.0-4ubuntu2_all.deb"],
255260
)
256261

262+
###############################################################################
263+
# Graphviz deb package (cmake release; bundles all graphviz .so files so
264+
# dot_builtins runs without system graphviz installation)
265+
# Uses download_deb from @download_utils at a commit that includes
266+
# data.tar.gz support in download/deb/repository.bzl.
267+
###############################################################################
268+
deb(
269+
name = "graphviz_deb",
270+
build = "//third_party/graphviz:graphviz.BUILD",
271+
integrity = "sha256-Jk5gSqo8l0INoY+kr1ZAsi2WhZY8LlAFlEag54H3Q2Q=",
272+
urls = ["https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/12.2.1/ubuntu_24.04_graphviz-12.2.1-cmake.deb"],
273+
)
274+
257275
register_toolchains(
258276
"//bazel/rules/rules_score:sphinx_default_toolchain",
259277
)

bazel/rules/rules_score/private/sphinx_module.bzl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,35 @@ def _score_html_impl(ctx):
236236
"--log-level",
237237
get_log_level(ctx),
238238
]
239+
240+
# Wire in the hermetic graphviz deb (dot_builtins + bundled shared libs).
241+
# conf.template.py resolves all three env vars (GRAPHVIZ_DOT,
242+
# LD_LIBRARY_PATH, LTDL_LIBRARY_PATH) from execroot-relative to absolute
243+
# paths so dot_builtins can load its plugins without a system installation.
244+
_dot_suffix = "/usr/bin/dot_builtins"
245+
graphviz_files = ctx.files.graphviz
246+
dot_binary = None
247+
for f in graphviz_files:
248+
if f.path.endswith(_dot_suffix):
249+
dot_binary = f
250+
break
251+
if not dot_binary:
252+
fail("graphviz target {} must provide usr/bin/dot_builtins".format(ctx.attr.graphviz.label))
253+
254+
graphviz_prefix = dot_binary.path[:-len(_dot_suffix)]
255+
graphviz_env = {
256+
"GRAPHVIZ_DOT": dot_binary.path,
257+
"LD_LIBRARY_PATH": graphviz_prefix + "/usr/lib",
258+
"LTDL_LIBRARY_PATH": graphviz_prefix + "/usr/lib/graphviz",
259+
}
260+
html_inputs = html_inputs + graphviz_files
261+
239262
ctx.actions.run(
240263
inputs = html_inputs,
241264
outputs = [sphinx_html_output],
242265
arguments = html_args + [args],
266+
env = graphviz_env,
267+
use_default_shell_env = True,
243268
progress_message = "Building HTML: %s" % ctx.label.name,
244269
executable = sphinx_toolchain.sphinx.files_to_run.executable,
245270
tools = [
@@ -331,6 +356,12 @@ _score_html = rule(
331356
"destination paths relative to the Sphinx source root. Exactly one " +
332357
"file per label. Mirrors sphinx_docs.renamed_srcs from rules_python.",
333358
),
359+
graphviz = attr.label(
360+
default = Label("@graphviz_deb//:all"),
361+
allow_files = True,
362+
doc = "Graphviz cmake-release deb files (dot_builtins binary + bundled libs). " +
363+
"Provides a hermetic 'dot' binary without requiring a system graphviz installation.",
364+
),
334365
),
335366
toolchains = ["//bazel/rules/rules_score:toolchain_type"],
336367
)

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

Lines changed: 66 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
@@ -164,9 +208,29 @@
164208
plantuml = f"{plantuml_path} -Playout=smetana"
165209
plantuml_output_format = "svg_obj"
166210

167-
import shutil as _shutil
211+
# ---------------------------------------------------------------------------
212+
# Graphviz (sphinx.ext.graphviz)
213+
# ---------------------------------------------------------------------------
214+
# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the hermetic
215+
# dot_builtins binary from @graphviz_deb. The path is execroot-relative, so
216+
# we resolve it to an absolute path here so it remains valid after any cwd
217+
# change that Sphinx may perform during the build.
218+
graphviz_dot = _resolve_execroot_path(
219+
os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot"
220+
)
168221

169-
graphviz_dot = os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot"
222+
# LD_LIBRARY_PATH and LTDL_LIBRARY_PATH are set by the Bazel rule as
223+
# execroot-relative paths. We mutate os.environ (not just a local) because
224+
# sphinx.ext.graphviz spawns `dot` as a child process that inherits these
225+
# variables to locate the bundled shared libraries and plugins. Each
226+
# component is resolved to absolute so it stays valid if Sphinx changes cwd
227+
# before spawning the dot subprocess.
228+
for _env_var in ("LD_LIBRARY_PATH", "LTDL_LIBRARY_PATH"):
229+
_env_val = os.environ.get(_env_var, "")
230+
if _env_val:
231+
os.environ[_env_var] = ":".join(
232+
_resolve_execroot_path(p) for p in _env_val.split(":")
233+
)
170234

171235
# HTML theme
172236
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 download_deb rule from @download_utils extracts the Graphviz cmake
16+
# release .deb and uses graphviz.BUILD as its top-level BUILD file.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
# graphviz_deb rule. It exposes dot_builtins and required bundled shared
16+
# libraries for Sphinx graphviz rendering in a hermetic way.
17+
18+
package(default_visibility = ["//visibility:public"])
19+
20+
# The actual graphviz rendering binary (not the dot wrapper/launcher).
21+
# Uses RUNPATH $ORIGIN/../lib to find bundled shared libraries.
22+
filegroup(
23+
name = "dot_binary",
24+
srcs = ["usr/bin/dot_builtins"],
25+
)
26+
27+
# Bundled graphviz shared libraries (libgvc, libcgraph, libcdt, libpathplan, libxdot).
28+
# These are found automatically by dot_builtins via RUNPATH $ORIGIN/../lib.
29+
filegroup(
30+
name = "core_libs",
31+
srcs = glob(["usr/lib/*.so*"]),
32+
)
33+
34+
# Graphviz plugin shared libraries (libgvplugin_core, libgvplugin_dot_layout, etc.).
35+
# Loaded at runtime via libltdl; requires LTDL_LIBRARY_PATH=usr/lib/graphviz.
36+
filegroup(
37+
name = "plugin_libs",
38+
srcs = glob(["usr/lib/graphviz/*.so*"]),
39+
)
40+
41+
# All graphviz files needed to run dot_builtins.
42+
filegroup(
43+
name = "all",
44+
srcs = [
45+
":core_libs",
46+
":dot_binary",
47+
":plugin_libs",
48+
],
49+
)

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)