Mounts extension internals#
This page is for maintainers of score_mounts. User-facing bundle and mount
semantics are documented in Documentation bundles and mounts; BUILD usage is in
How to use bundles.
Architecture and manifest contract#
docs.bzl owns bundle graph traversal. Its composition-manifest rule turns the
DocsBundleInfo provider graph and each consumer placement into one JSON composition
manifest. The same manifest supplies both mount placement and document-to-bundle
metadata. Python deliberately receives paths and stable bundle metadata rather than
Bazel providers, so it does not reconstruct Bazel repository names at Sphinx runtime.
Each manifest entry contains:
src_root— the source directory relative to the main workspace or the sandbox execroot;runtime_path— the Bazel short path used when a staged directory must be walked;mount_atandattach_to— the already-composed Sphinx placement; andentry_doc— the canonical entry document declared by the source bundle.external— whether the directory belongs to another Bazel module;bundle— the declaring bundle’s Bazel label, name, and direct targets;root_bundle— whether this physical entry belongs to the root bundle of the current composition. This is composition-specific: the same bundle can be a root in a standalone manifest and a child in another composition.
Standalone bundle Needs actions receive a local-only view containing the bundle’s own root entries; composed builds receive the complete active composition. Both views use the same schema and runtime resolver.
At config-inited, score_mounts resolves all directory source mounts before
constructing config.mounts. A mount below Sphinx’s primary source directory
is added to the primary exclude_patterns as a relative <mount>/** pattern.
A mount below another mount is added to the parent entry’s sphinx-mounts
exclude patterns. This prevents a document from being discovered by both its
containing directory walk and its owning mount. Because the exclusions are
directory patterns rather than a snapshot of source files, newly created files
remain visible to live preview.
Explicit docs_bundle(srcs = [...]) entries remain in file-list mode and are
not included in these directory exclusions. They are intended for generated
sources outside the primary source tree; an explicitly mounted workspace file
below a walked root is a known limitation and would need exact-file exclusions.
Document-to-bundle mapping#
After Sphinx has updated its environment, score_mounts exposes
get_document_bundles(app). The result maps each actually discovered
bundle-owned docname to one bundle instance. Mounted source docnames come from
the mount integration’s recorded output, while primary docnames come from
Sphinx’s discovery set. Documents discovered through data-only mounts are
intentionally omitted because those mounts provide auxiliary files rather than
bundle-owned source roots. This avoids assigning a bundle to skipped,
conflicting, or data-only mounts. The matcher in score_metamodel will
consume this mapping without inspecting paths or mount configuration.
The rule rejects conflicting final placements before Sphinx starts. A mount
without attach_to is attached to the index document beside its
mount_at; attach_to overrides that target. The Python
extension may therefore preserve declaration order and only translates each
manifest entry into the sphinx_mounts configuration format.
Directory resolution#
score_mounts._resolver.resolve_walk_dir selects the directory that
sphinx_mounts walks. The source directory is never copied.
Build context |
Directory used |
|---|---|
|
live |
sandboxed build |
staged |
|
sibling repository in the runfiles tree |
External short paths begin with ../<repo>+ relative to the runfiles
_main directory. The resolver therefore prefixes _main before turning
that path into an absolute path. This distinction is covered by unit tests and
the public docs_bzl integration suite; do not resolve an external runtime path
relative to the manifest file, because the manifest can live below a Bazel
package directory.
Incremental builds use the same resolver in src/docs_cli/cli.py to add every
mounted directory to sphinx-autobuild’s watch list. Keep these two call
sites aligned when the manifest contract changes.
TOML synchronization#
score_mounts writes structured mount entries to config.mounts during
config-inited. score_sync_toml then serializes them through
needs-config-writer into the repository-root ubproject.toml. The
generated file is intentionally shared by the host and all in-tree bundles so
IDE tooling can discover one configuration while walking up from either source
tree.
The ordering is significant: score_mounts runs before sphinx_mounts so
the manifest is authoritative at build time, and score_sync_toml runs after
it so the TOML writer sees the resolved structured entries.
Why bundles are not materialized#
A source_dir bundle already has the mount-relative layout on disk.
Materializing a second directory would duplicate its files, point navigation at
a generated copy, and add a build action without changing the Sphinx input.
Reconsider materialization only if a future bundle cannot be represented by one
existing directory, for example for a filtered file set, a custom
strip_prefix, or generated provider content. Prefer native filtering support
in sphinx_mounts first. If a copy action becomes necessary, extend the
manifest with a directory artifact and keep its identity stable for placement
deduplication.
Validation#
Relevant checks are:
bazel test //src/extensions/score_mounts:score_mounts_testsfor manifest parsing and path resolution;.venv_docs/bin/python -m pytest -vv src/tests/docs_bzl/test_nested_bundles.py src/tests/docs_bzl/test_external_bundle.py src/tests/docs_bzl/test_invalid_bundle_placements.pyfor mounted rendering, toctree attachment, external bundles underbazel run, sandboxed external builds, and placement conflicts; andbazel test //...for the internal extension unit tests.