Integration Guide
Build Flow
The diagram below shows how input files flow through the Bazel rules to produce the final outputs.
Toolchain Setup
rules_score ships a default Sphinx toolchain, registered by score_tooling’s
own MODULE.bazel. You need nothing to get a working Sphinx build — plain
sphinx-needs, TRLC and PlantUML documentation builds out of the box for any
module that depends on score_tooling, with no toolchain setup at all.
Registering your own toolchain is only needed when you want additional
Sphinx extensions (e.g. Breathe for Doxygen, a custom theme) that the default
doesn’t carry. Because Bazel resolves toolchains from the root module first,
a toolchain registered by your own MODULE.bazel always wins over
score_tooling’s default — no special opt-out required.
Adding extensions
Use the score_sphinx_toolchain macro to extend the default dependency set
instead of reproducing it:
MODULE.bazel:
bazel_dep(name = "score_tooling", version = "1.3.2")
# Dependency providing your custom Sphinx extension
bazel_dep(name = "score_docs_as_code", version = "3.0.1", dev_dependency = True)
register_toolchains("//:my_toolchain")
BUILD:
load("@score_tooling//bazel/rules/rules_score:sphinx_toolchain.bzl", "score_sphinx_toolchain")
score_sphinx_toolchain(
name = "my_toolchain",
extra_deps = [
"@score_docs_as_code//src/extensions/score_sphinx_bundle",
],
)
This emits my_toolchain_binary (the Sphinx build binary: the shared
defaults plus extra_deps), my_toolchain_info (the sphinx_toolchain
target) and my_toolchain (the toolchain() itself) — register the last
one.
Diagnosing which toolchain won
bazel cquery --toolchain_resolution_debug='.*rules_score.*' //:my_target
Look for the Selected ... toolchain line under
@score_tooling//bazel/rules/rules_score:toolchain_type.
Replacing the dependency set entirely
If your extensions conflict with the shared defaults — e.g. a different pip
hub pinning an incompatible version of a shared package — pass deps
instead of extra_deps to bypass sphinx_base_deps entirely and supply
the full list yourself:
score_sphinx_toolchain(
name = "my_toolchain",
conf_template = "//:my_conf.template.py",
deps = [
"@score_tooling//bazel/rules/rules_score:sphinx_module_ext",
"@my_pip_hub//sphinx:pkg",
"@my_pip_hub//my_custom_extension:pkg",
# ... full list; sphinx_base_deps is not included in this mode
],
)
extra_deps and deps are mutually exclusive — passing both fails at
load time.
score_sphinx_toolchain parameters:
name— name of the emittedtoolchain()target (mandatory)extra_deps— extend mode: extra deps added on top of the shared default set (optional; default:[])deps— replace mode: exact dep list, bypassing the shared defaults (optional; default: not set)extra_data— extra data files/targets for the Sphinx build binary (optional; default:[])conf_template— Label toconf.pytemplate (optional; default:@score_tooling//bazel/rules/rules_score:templates/conf.template.py)package_collisions— forwarded to the generatedpy_binary(optional; default:"warning")any other keyword argument (e.g.
visibility,exec_compatible_with,target_compatible_with) is forwarded to thetoolchain()target
Assembling a toolchain by hand
For cases the macro doesn’t fit, use the underlying sphinx_toolchain rule
directly — it only bundles a Sphinx binary and a conf.py template into the
provider the rules consume; you own the py_binary and toolchain():
load("@aspect_rules_py//py:defs.bzl", "py_binary")
load("@score_tooling//bazel/rules/rules_score:sphinx_toolchain.bzl", "sphinx_toolchain")
py_binary(
name = "score_build",
srcs = ["@score_tooling//bazel/rules/rules_score:src/sphinx_wrapper.py"],
main = "@score_tooling//bazel/rules/rules_score:src/sphinx_wrapper.py",
visibility = ["//visibility:public"],
deps = [
"@score_tooling//bazel/rules/rules_score:sphinx_base_deps",
"@score_docs_as_code//src/extensions/score_sphinx_bundle",
],
)
sphinx_toolchain(
name = "score_sphinx_toolchain",
sphinx = ":score_build",
)
toolchain(
name = "my_toolchain",
toolchain = ":score_sphinx_toolchain",
toolchain_type = "@score_tooling//bazel/rules/rules_score:toolchain_type",
visibility = ["//visibility:public"],
)
sphinx_toolchain parameters:
sphinx— Label to the Sphinx build binary (optional; default:@score_tooling//bazel/rules/rules_score:raw_build)conf_template— Label toconf.pytemplate (optional; default:@score_tooling//bazel/rules/rules_score:templates/conf.template.py)
The HTML-merge tool used to combine dependency HTML trees is a fixed,
private implementation detail of sphinx_module — it is not part of
SphinxInfo and cannot be overridden.
Cross-module dependencies
sphinx_module and dependable_element targets reference each other via
deps to produce merged HTML output:
<name>/html/
├── index.html
├── _static/
├── dependency1/ ← merged from first dep
└── dependency2/ ← merged from second dep
Complete Example
load("@score_tooling//bazel/rules/rules_score:rules_score.bzl",
"architectural_design", "assumed_system_requirements",
"assumptions_of_use", "component", "component_requirements",
"dependability_analysis", "dependable_element",
"feature_requirements", "fmea", "unit")
# Requirements
assumed_system_requirements(name = "sys_req", srcs = ["docs/sys_req.trlc"])
feature_requirements(name = "features", srcs = ["docs/features.trlc"],
deps = [":sys_req"])
component_requirements(name = "reqs", srcs = ["docs/reqs.trlc"],
deps = [":features"])
assumptions_of_use(name = "aous", srcs = ["docs/aous.trlc"],
requirements = [":features"])
# Architecture
architectural_design(name = "arch",
static = ["docs/arch.puml"],
dynamic = ["docs/sequence.puml"],
public_api = ["docs/public_api.puml"])
# Safety analysis
fmea(name = "my_fmea", arch_design = ":arch",
controlmeasures = ["docs/controls.trlc"],
failuremodes = ["docs/failures.trlc"],
root_causes = ["docs/fta.puml"])
dependability_analysis(name = "analysis", fmea = [":my_fmea"])
# Implementation
cc_library(name = "kvs_lib", srcs = ["kvs.cpp"], hdrs = ["kvs.h"])
cc_test(name = "kvs_test", srcs = ["kvs_test.cpp"], deps = [":kvs_lib"])
# Structure
unit(name = "kvs_unit", unit_design = [":kvs_unit_design"],
implementation = [":kvs_lib"], tests = [":kvs_test"])
component(name = "kvs_component", requirements = [":reqs"],
components = [":kvs_unit"], tests = [])
# SEooC
dependable_element(
name = "persistency_kvs",
integrity_level = "B",
assumptions_of_use = [":aous"],
requirements = [":reqs"],
architectural_design = [":arch"],
dependability_analysis = [":analysis"],
components = [":kvs_component"],
tests = [],
deps = ["@score_process//:score_process_module"],
)
Build and test:
bazel build //:persistency_kvs
bazel test //:persistency_kvs
# HTML output: bazel-bin/persistency_kvs/html/
Design Rationale
Two-Tier Architecture — Generic
sphinx_modulefor flexibility; specialised artifact rules for safety-critical work productsDependency Management — Automatic cross-referencing and HTML merging across modules
Standardisation —
dependable_elementenforces a consistent structure for all safety documentationTraceability — Sphinx-needs integration enables bidirectional traceability
Automation — Index generation, symlinking, and
conf.pymanagement are automaticBuild System Integration — Bazel ensures reproducible, cacheable documentation builds
Reference implementation: examples/seooc in the score-tooling repository.
—
Hermetic Diagram Tools (Graphviz and PlantUML)
The Sphinx HTML action shells out to two diagram tools at runtime (inside
Bazel actions): dot from Graphviz and PlantUML. Both are hermetic —
i.e.no host installation required. The two tools use different
delivery mechanisms, described below.
Graphviz / dot
Source and packaging
Graphviz now comes directly from the docs runtime sysroot
(@docs_runtime//:flat), built with rules_distroless from the
apt.install(dependency_set = "docs_runtime", ...) tag in //MODULE.bazel.
The Sphinx action does not call dot directly; it uses
//third_party/docs_runtime:dot — an exec_in_sysroot wrapper that
unpacks the sysroot archive and runs /usr/bin/dot inside it through
fakechroot.
Where the files land (execroot-relative paths)
bazel-bin/third_party/docs_runtime/dot ← GRAPHVIZ_DOT env var
bazel-bin/third_party/docs_runtime/dot_sysroot/ ← unpacked docs_runtime rootfs
usr/bin/dot
usr/lib/graphviz/...
usr/bin/fakechroot
Wiring into the Sphinx action
The Bazel rule sets one variable:
Env var |
Content |
|---|---|
|
Path to the |
The value points to the hermetic wrapper executable. The wrapper resolves and
executes graphviz from the sysroot itself, so no custom LD_LIBRARY_PATH /
GVBINDIR wiring is required in the Sphinx action.
Resolving paths in conf.py
GRAPHVIZ_DOT is set as an execroot-relative path. Because Sphinx changes
the process working directory during the build, it would break if used as-is.
conf.template.py converts it to a stable absolute path with a single
os.path.abspath() call at module import time, when Bazel guarantees the
action’s cwd still equals the execroot (before Sphinx performs any
os.chdir()). See Architecture §”Hermetic tool path
resolution” for the full rationale.
PlantUML
Source and packaging
PlantUML is fetched from Maven Central via rules_jvm_external
(declared in MODULE.bazel). It is wrapped as a java_binary at
//third_party/plantuml:plantuml in third_party/plantuml/BUILD.
The sphinx_module rule passes the target as an action tool
(attr.label(executable = True, cfg = "exec")), exactly like the hermetic
graphviz dot. It is not a runfile of the sphinx-build binary.
Wiring into the Sphinx action
The Bazel rule sets one variable (mirroring GRAPHVIZ_DOT):
Env var |
Content |
|---|---|
|
Execroot-relative path to the |
PLANTUML_BIN_RLOC (the short_path rlocation key) is also set, but is
used only for diagnostic logging.
Resolving the path in conf.py
PLANTUML_BIN is an execroot-relative path. As with GRAPHVIZ_DOT,
conf.template.py converts it to an absolute path with a single
os.path.abspath() call — Bazel guarantees the action’s cwd equals the
execroot when conf.py is imported, before Sphinx performs any
os.chdir().
Connecting PlantUML to Graphviz
Once both paths are resolved, conf.template.py assembles the PlantUML
command:
plantuml = f"{plantuml_path} -graphvizdot {graphviz_dot}"
The -graphvizdot flag makes PlantUML use the hermetic dot binary for
diagram layout instead of its bundled Java port (Smetana). This ensures the
graphviz version is identical for both sphinx.ext.graphviz directives and
PlantUML diagrams. There is no Smetana fallback: the hermetic dot is the
single rendering path.