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_at and attach_to — the already-composed Sphinx placement; and

  • entry_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, explicit primary Need ID, 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.

Each bundle declares a private <name>.__internal__.files target whose DefaultInfo.files contains its implementation artifacts. The source-link cache scans exactly that file set. The public bundle manifest does not expose implementation file lists.

Explicitly selected code targets contribute their declared sources and headers. Transitive dependency targets are filtered to the bundle’s repository and BUILD package, including subpackages. Local targets reached through foreign dependencies remain included. The boundary is independent of source_dir; nested documentation bundles keep their own file sets. Source/header attributes resolve filegroups and generated outputs without inspecting generator inputs.

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 bundle metadata pass in score_metamodel consumes 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

bazel run, in-tree

live BUILD_WORKSPACE_DIRECTORY / src_root

sandboxed build

staged execroot / src_root

bazel run, external

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_tests for 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.py for mounted rendering, toctree attachment, external bundles under bazel run, sandboxed external builds, and placement conflicts; and

  • bazel test //... for the internal extension unit tests.