|
35 | 35 | # --------------------------------------------------------------------------- |
36 | 36 |
|
37 | 37 |
|
38 | | -# Capture the current working directory at module import time. |
39 | | -# In Bazel action context, cwd == execroot. In IDE/non-Bazel runs, cwd is |
40 | | -# the current directory. This is captured once to avoid repeated resolution. |
41 | | -_EXECROOT = Path.cwd() |
| 38 | +# Resolve execroot-relative paths against the Bazel execroot. sphinx_wrapper.py |
| 39 | +# exports SPHINX_BAZEL_EXECROOT (the action cwd captured before Sphinx changes |
| 40 | +# into the generated source tree). Fall back to the current cwd for non-Bazel |
| 41 | +# / IDE runs where the variable is absent. |
| 42 | +_EXECROOT = Path(os.environ.get("SPHINX_BAZEL_EXECROOT", "") or Path.cwd()) |
42 | 43 |
|
43 | 44 |
|
44 | 45 | def _resolve_execroot_path(path_value: str) -> str: |
45 | 46 | """Resolve an execroot-relative path to an absolute filesystem path. |
46 | 47 |
|
47 | 48 | Bazel passes action inputs as paths relative to the execroot (e.g. |
48 | | - ``external/+_repo_rules2+graphviz_deb/usr/bin/dot_builtins``). Those |
49 | | - paths are only valid when the process' cwd is the execroot — which is |
50 | | - not guaranteed once Sphinx changes directories during the build. |
| 49 | + ``external/+_repo_rules+graphviz_deb/usr/bin/dot_builtins``). Sphinx changes |
| 50 | + into the generated source tree before importing conf.py, so the process cwd |
| 51 | + is no longer the execroot. We resolve against ``_EXECROOT`` (captured by the |
| 52 | + wrapper before that chdir) so the paths stay valid for the ``dot`` |
| 53 | + subprocess. |
51 | 54 |
|
52 | | - This function makes them absolute so they work regardless of cwd. |
53 | 55 | Absolute paths and plain command names (e.g. ``dot``) are returned |
54 | 56 | unchanged. |
55 | 57 | """ |
56 | 58 | p = Path(path_value) |
57 | 59 | if p.is_absolute(): |
58 | 60 | return str(p) |
59 | 61 | if path_value.startswith("external/") or path_value.startswith("bazel-out/"): |
60 | | - # First try cwd-as-execroot (fast path). |
61 | | - candidate = (_EXECROOT / p).resolve() |
62 | | - if candidate.exists(): |
63 | | - return str(candidate) |
64 | | - |
65 | | - # If cwd is nested under bazel-out, walk upward and locate the first |
66 | | - # parent that contains the requested relative path. |
67 | | - for parent in [_EXECROOT, *_EXECROOT.parents]: |
68 | | - candidate = (parent / p).resolve() |
69 | | - if candidate.exists(): |
70 | | - return str(candidate) |
71 | | - |
72 | | - # Fallback: preserve previous behavior even if the file does not exist |
73 | | - # yet (keeps logging/debug output deterministic). |
74 | 62 | return str((_EXECROOT / p).resolve()) |
75 | 63 | return path_value |
76 | 64 |
|
@@ -100,7 +88,6 @@ def _resolve_execroot_path(path_value: str) -> str: |
100 | 88 | "sphinxcontrib.plantuml", |
101 | 89 | "trlc", |
102 | 90 | "clickable_plantuml", |
103 | | - "sphinx.ext.graphviz", |
104 | 91 | ] |
105 | 92 |
|
106 | 93 | # MyST parser extensions |
@@ -205,38 +192,36 @@ def _resolve_execroot_path(path_value: str) -> str: |
205 | 192 | f"Could not find plantuml binary via runfiles lookup. Searched: {searched}." |
206 | 193 | ) |
207 | 194 |
|
208 | | -# Use PlantUML's built-in Smetana layout engine (Java port of Graphviz). |
209 | | -# This avoids requiring an external dot binary in the Bazel sandbox. |
210 | | -plantuml = f"{plantuml_path} -Playout=smetana" |
211 | | -plantuml_output_format = "svg_obj" |
212 | | - |
213 | 195 | # --------------------------------------------------------------------------- |
214 | | -# Graphviz (sphinx.ext.graphviz) |
| 196 | +# PlantUML + hermetic dot |
215 | 197 | # --------------------------------------------------------------------------- |
216 | | -# GRAPHVIZ_DOT is set by the Bazel sphinx_module rule to point at the hermetic |
217 | | -# dot_builtins binary from @graphviz_deb. The path is execroot-relative, so |
218 | | -# we resolve it to an absolute path here so it remains valid after any cwd |
219 | | -# change that Sphinx may perform during the build. |
220 | | -# If GRAPHVIZ_DOT is absent, force a known-invalid dot path so Sphinx fails |
221 | | -# clearly on graphviz directives instead of silently using host-installed dot. |
| 198 | +# GRAPHVIZ_DOT is set by sphinx_module on linux_x86_64 to the hermetic |
| 199 | +# dot_builtins binary from @graphviz_deb. When present, PlantUML is told to |
| 200 | +# use it directly via -graphvizdot, giving native Graphviz layout quality for |
| 201 | +# all UML diagram types. LD_LIBRARY_PATH / LTDL_LIBRARY_PATH are resolved to |
| 202 | +# absolute paths here so they remain valid in the dot_builtins subprocess that |
| 203 | +# PlantUML spawns (Sphinx may have chdir'd before then). |
| 204 | +# On other platforms (e.g. arm64, macOS) GRAPHVIZ_DOT is absent and PlantUML |
| 205 | +# falls back to its built-in Smetana layout engine (pure-Java, no dot needed). |
222 | 206 | if "GRAPHVIZ_DOT" in os.environ: |
223 | | - graphviz_dot = _resolve_execroot_path(os.environ["GRAPHVIZ_DOT"]) |
224 | | - graphviz_output_format = "svg" |
225 | | - |
226 | | - # LD_LIBRARY_PATH and LTDL_LIBRARY_PATH are set by the Bazel rule as |
227 | | - # execroot-relative paths. We mutate os.environ (not just a local) because |
228 | | - # sphinx.ext.graphviz spawns `dot` as a child process that inherits these |
229 | | - # variables to locate the bundled shared libraries and plugins. Each |
230 | | - # component is resolved to absolute so it stays valid if Sphinx changes cwd |
231 | | - # before spawning the dot subprocess. |
232 | | - for _env_var in ("LD_LIBRARY_PATH", "LTDL_LIBRARY_PATH"): |
233 | | - _env_val = os.environ.get(_env_var, "") |
234 | | - if _env_val: |
235 | | - os.environ[_env_var] = ":".join( |
236 | | - _resolve_execroot_path(p) for p in _env_val.split(":") |
237 | | - ) |
| 207 | + _dot_path = Path(_resolve_execroot_path(os.environ["GRAPHVIZ_DOT"])) |
| 208 | + # Derive library search paths from the binary location so the rule passes |
| 209 | + # only GRAPHVIZ_DOT and conf.py stays self-contained. |
| 210 | + # The graphviz cmake deb installs: |
| 211 | + # usr/bin/dot_builtins ← GRAPHVIZ_DOT points here |
| 212 | + # usr/lib/*.so* ← LD_LIBRARY_PATH (core shared libs) |
| 213 | + # usr/lib/graphviz/*.so* ← LTDL_LIBRARY_PATH (layout/render plugins) |
| 214 | + _usr_dir = _dot_path.parent.parent # usr/bin → parent → usr |
| 215 | + os.environ["LD_LIBRARY_PATH"] = str(_usr_dir / "lib") |
| 216 | + os.environ["LTDL_LIBRARY_PATH"] = str(_usr_dir / "lib" / "graphviz") |
| 217 | + plantuml = f"{plantuml_path} -graphvizdot {_dot_path}" |
238 | 218 | else: |
239 | | - graphviz_dot = "/__hermetic_graphviz_not_configured__/dot" |
| 219 | + logger.warning( |
| 220 | + "GRAPHVIZ_DOT not set; PlantUML falling back to Smetana layout engine. " |
| 221 | + "Hermetic dot (@graphviz_deb) is only available on linux_x86_64." |
| 222 | + ) |
| 223 | + plantuml = f"{plantuml_path} -Playout=smetana" |
| 224 | +plantuml_output_format = "svg_obj" |
240 | 225 |
|
241 | 226 | # HTML theme |
242 | 227 | html_theme = "sphinx_rtd_theme" |
|
0 commit comments