Motivation¶
sphinx-mounts exists to bridge the gap between modern build systems — particularly Bazel — and the IDE-extension layer that has grown up around Docs-as-Code workflows and the traceability work demanded by safety-critical software projects.
Two valid wishes in tension¶
Sphinx is deliberately built around a project — a tree of source
files with a known root, an index.rst, and a docname space that
mirrors how the documentation reads end-to-end. Users want that
structure; it is what makes the narrative flow legible to readers.
Build systems are built around the opposite premise. In tools like Bazel, composability — declaring “this artefact depends on these files and those groups of files” — is the core principle. The knowledge of how content fits together lives inside the build system, not in the source tree.
The two wishes are in diametrical conflict, and both are valid.
A third axis sits on top. For pure requirements management — say, a Sphinx-Needs corpus extracted to back a safety statement — the documentation structure barely matters. What matters is the requirements graph and its traceability links; the file or chapter a given requirement lives in is incidental to the safety claim.
But documentation is still read by humans, and increasingly by agents and LLMs, and both benefit from a coherent narrative. Requirements themselves typically reference files — images, architecture diagrams, reference standards — that live on the filesystem. Exchange formats such as ReqIF model these attachments as first-class parts of the requirement. The dependency on file paths (even relative ones) cannot be elegantly delegated to the build system: the documentation source needs to see the attached file at a known relative location.
So a real-world Sphinx project for safety-critical work has to satisfy all three at once — narrative structure for readers, composability for the build system, and concrete on-disk file dependencies for requirements that point at images and diagrams.
The gap: Bazel sandbox vs. live IDE feedback¶
A build system like Bazel is the right tool to express
“this documentation depends on those files generated by that rule”.
It computes a complete, hermetic graph of inputs and outputs and
runs commands inside sandboxes. For a Sphinx project that gathers
documentation from many sources — generated API references,
requirement bundles from sibling repositories, schema-driven
needs imports — Bazel cleanly answers what gets built and
where the results land.
What Bazel does not answer well is editing feedback. A developer authoring an RST or Markdown file inside a generated bundle wants their editor to:
highlight broken cross-references as they type;
complete docnames and
:ref:labels against the actual project;show
needsmetadata, traceability links, and lint diagnostics without runningsphinx-buildin a Bazel sandbox continuously;present a faithful preview of how the file will render in the final docs.
That is what an IDE extension is for. Extensions such as ubCode
are designed to read a project’s configuration and its source tree
and provide exactly the above. The catch is that they cannot do it
when the project’s configuration lives inside an executable Python
file (conf.py) that the IDE would have to evaluate — with all
of the project’s transitive dependencies — to learn anything about
the project’s structure.
Why the usual workarounds fall short¶
The usual answer to “I have RST generated outside srcdir” is to
copy or symlink those bundles into the Sphinx source tree before
the build. That works for sphinx-build, but it costs more than
it appears to:
IDE tools have to replicate the copy before they can read the content, doubling the work and adding state that is easy to get wrong.
Duplication breeds inconsistency. Two on-disk copies of a file drift, caches go stale, and the linter ends up disagreeing with the build.
A construction step is required. Something has to run before Sphinx to put the files in place — yet another script the project has to maintain and version.
Index files have to be generated (dirtying the working tree with synthesised
index.rstfiles), or globbed toctrees have to be used (which silently swallow missing documents and obscure the structure).Orphan-page warnings become routine, because the host project cannot easily know which files the staging step deposited.
Custom Sphinx extensions narrowly tailored to one source format — for example sphinx-codelinks extracting traceability from source comments, or Doxylink bridging into Doxygen XML — are a legitimate complement when the source is specialised enough. They do not, however, generalise to “make this directory of finished RST or Markdown visible to Sphinx without copying it” — which is the gap sphinx-mounts fills.
Three properties fall out of all this:
A declarative configuration that every tool — Sphinx, IDE, linter, indexer — can read without evaluating a Python file.
A zero-copy mechanism — files are read in place from their original location, with no staging directory, no symlink farm, and no pre-build construction step.
A binding mechanism that leaves the source tree untouched — mount points are virtual, no
index.rstis synthesised, no toctree is globbed, no orphan-page warnings are emitted.
How sphinx-mounts closes the gap¶
sphinx-mounts moves the one piece of configuration the IDE
actually needs — “where do the documentation source files live and
under what docname prefix do they appear?” — out of conf.py and
into a static ubproject.toml. That TOML
is:
read by Sphinx at build time to set up the mounts;
read by IDE extensions at edit time to resolve cross-references and run live diagnostics;
read by any other tool — linters, CI gates, doc indexers — that wants to understand the project without running Python.
A single file is then the shared contract between the build system
and the editor. The IDE extension gets the entire dependency
picture: it knows which directories Bazel will populate, where on
disk those directories will be (the bazel-bin / bazel-out
symlinks are workspace-relative and identical on every OS), and
what docname namespace each will inhabit after the build. One
Bazel target — typically a filegroup that materialises every
generated bundle the host project mounts — is enough to give the
IDE the full picture.
Once that target has been built once, the IDE extension can:
preview a mounted RST or Markdown file in place, exactly as Sphinx will render it;
resolve
:doc:/:ref:/ Sphinx-Needs links between the host project and every mounted bundle;lint and validate a bundle in isolation, against the host’s configuration;
analyse traceability — for the requirement / verification chains central to safety-critical projects — without firing off a full
sphinx-build.
The same ubproject.toml is also what Sphinx itself uses inside
the Bazel sandbox: the build is driven by bazel-bin paths, and
sphinx-mounts reads them in place rather than forcing a copy
step. The sandboxed build and the editor see the same files, with
the same docname layout — no drift between “what the IDE thinks the
project looks like” and “what Sphinx ultimately builds”.
Why not just use sphinx-collections?¶
sphinx-collections solves a superset of the same problem — getting external content into a Sphinx build — and is a sensible choice for projects that need more than what sphinx-mounts offers. The two tools sit at different points on the generic vs. focused axis.
sphinx-collections is a generic, driver-based content-collector.
At build time it runs one of its many drivers per configured
collection — copy_file, copy_folder, symlink, git,
string, function, jinja, report — and writes the
result into _collections/<name>/ inside srcdir so Sphinx can
read it. The drivers are the API: pick the right one for where your
content lives (a local path, a git URL, a Python function, a Jinja
template, …) and sphinx-collections materialises it for Sphinx.
sphinx-mounts is the opposite trade-off — one mechanism, no
materialisation. It assumes some external system (Bazel, Make,
rsync, a sibling repo, your file manager) has already put files
at a known filesystem path; the extension’s only job is to make that
path visible to Sphinx’s docname resolver in place. Nothing is
copied, symlinked, cloned, generated, or written into srcdir.
Practical consequences:
Aspect |
|
|
|---|---|---|
Content acquisition |
In Sphinx, via drivers (clone, copy, render, …) |
Out of Sphinx; the build system / a developer puts files in place beforehand |
On-disk location of mounted docs |
|
Wherever they originally live (read in place) |
Number of integration mechanisms |
8+ drivers covering many use cases |
1 — directory or file list on the filesystem |
Configuration surface |
One |
One declarative |
IDE story |
Editor sees |
Editor sees the original files directly; the same
|
Right fit when |
You want Sphinx itself to pull content from heterogeneous sources (git, URLs, templates, callbacks) and you are happy for those sources to be materialised directly into the Sphinx tree, with neither Sphinx nor the IDE retaining knowledge of the original-source mapping |
The “where do files come from” question is already answered by a separate build system or workflow, and you just want Sphinx to read them |
In short: pick sphinx-collections when Sphinx needs to fetch or
generate content as part of its own build; pick sphinx-mounts
when content already exists at a known path and you want Sphinx (and
every other tool reading ubproject.toml) to see it without an
intermediate staging step.
Why this matters in safety-critical projects¶
Docs-as-Code in regulated environments tends to come with strict constraints:
Traceability between requirements, specifications, implementation, and verification must be auditable and consistent across many repositories.
Distributed authoring is the norm — each component owns its own RST / Markdown source tree, contributed back into a host documentation build.
Reproducibility of the build is a hard requirement; the build system, not the developer’s local environment, must decide what goes in.
Fast feedback is what keeps a fast-moving project’s documentation in a buildable state. If the only way to see whether a cross-reference resolves is to wait on a multi-minute Bazel + Sphinx pipeline, errors accumulate.
sphinx-mounts is small in scope on purpose: it focuses on the one piece that lets all of these layers cooperate — making generated documentation sources visible to Sphinx in place, via a declarative config the IDE can also read — and lets Bazel, Sphinx, and the IDE each do what they are good at.
The compact picture¶
┌─────────────────────────────────────────────┐
│ Bazel rules │
│ - decide what's in the build │
│ - emit bundles to bazel-bin / bazel-out │
└────────────────────┬────────────────────────┘
│ both read ▼
┌───────────────┴──────────────────┐
│ ubproject.toml │
│ (static, declarative, │
│ shared by every consumer) │
└───────────────┬──────────────────┘
│
┌───────────────────┴───────────────────────┐
▼ ▼
┌───────────────────┐ ┌─────────────────────────┐
│ Sphinx + sphinx- │ │ ubCode / other IDE │
│ mounts in the │ │ extensions │
│ Bazel sandbox │ │ - live preview │
│ - read in place │ │ - cross-ref completion │
│ - no copy step │ │ - linting, traceability│
│ - write HTML │ │ - no sphinx-build run │
└───────────────────┘ └─────────────────────────┘
The developer experience this unlocks — generated, distributed documentation that nevertheless renders, lints, and traces immediately in the editor — is the entire point.