eclipse-score / eclipse-score/docs-as-code
Sharing project configuration with documentation bundles
- Dominant language
- Python
- Stars
- 10
- Forks
- 32
- Avg merge
- 23h 52m
- Merged PRs (30d)
- 52
Description
Status: proposed. This document describes a future implementation; the new
configuration interface is not implemented yet.
Prerequisite: [named documentation target families](name_concept.md). Implement
that independent naming and compatibility migration before this proposal.
## Problem and intended behavior
The public `docs()` and `docs_bundle()` macros live in [docs.bzl](docs.bzl).
Each source-bearing bundle has a local Needs export built from its own sources.
Standalone bundle exports currently generate a baseline Sphinx configuration
with the bundle name, an empty `project_url`, and the default metamodel. The root
bundle can use its project's configuration, but its local export does not receive
the explicit `docs(metamodel = ...)` argument. Bundle-local inventories also
deliberately export an empty `project_url`, even when Sphinx knows a project URL.
The proposal makes selected settings from `docs()` available to bundle-local
exports through internal targets associated with a named `docs()` invocation.
Configuration belongs to the project that defines and publishes the bundle.
Mounting that bundle into another
project does not change its standalone export's settings or publication metadata.
The combined project build continues to use the mounting project's configuration.
Initially, the shared settings are `project`, `project_url`, `metamodel`, and `deps`.
Publication metadata also includes the bundle's full placement path within its
defining project's documentation tree.
## Public interface
Settings remain arguments of the named `docs()` invocation established by
[name_concept.md](name_concept.md). Its public owner label is `//package:`;
its canonical incremental run target is `//package:.incremental`.
Bundles refer to the named `docs()` invocation through a new `docs` argument.
Users do not refer to configuration targets. Internally, the macro resolves the
owner to `.__internal__.docs_config` and other helpers. Neither a public
`docs_config` target nor a separate configuration macro is introduced.
```python
# //BUILD
load("@score_docs_as_code//:docs.bzl", "docs")
docs(
name = "docs",
project = "Example",
project_url = "https://example.org/example",
metamodel = "//:metamodel.yaml",
deps = ["//tools:documentation_helpers"],
bundles = [{
"bundle": "//components/engine:docs_bundle",
"mount_at": "components/engine",
}],
)
```
The default owner of a bundle is the root `docs()` invocation:
```python
# //components/engine/BUILD
load("@score_docs_as_code//:docs.bzl", "docs_bundle")
docs_bundle(
name = "docs_bundle",
source_dir = "docs",
visibility = ["//visibility:public"],
# docs defaults to //:docs in this bundle's repository.
)
```
The default is the declaring repository's `//:docs`, including when that
repository is an external Bzlmod dependency. It must not resolve to the macro
implementation's repository or to the repository mounting the bundle. There is
no search for an owner in ancestor packages.
For a project declared with `docs(name = "site", ...)` in `//project`, select
that invocation explicitly:
```python
docs_bundle(
name = "docs_bundle",
source_dir = "docs",
docs = "//project:site",
)
```
The root bundle generated by `docs()` always selects that invocation, including
when it has a custom name or is declared in a subdirectory. Selecting an owner on
an aggregating bundle does not implicitly change its children's owner selections.
Use `docs = None` for a standalone export without an owning publication:
```python
docs_bundle(
name = "standalone",
source_dir = "docs",
docs = None,
)
```
Owner selection has the following contract:
| Case | Behavior |
| --- | --- |
| Configured local export | Use the selected owner's shared settings and placement. |
| Missing owner or its generated helpers | Fail dependency analysis; declare the named `docs()` invocation, select another owner, or use `docs = None`. |
| Bundle absent from selected owner's mount tree | Fail with a diagnostic identifying the bundle and owner; do not guess a publication path. |
| Owner omits `project_url` | Fail the configured local export with a diagnostic requesting an explicit `docs(project_url = ...)`. |
| Owner omits `project` | Use the bundle name for its local export. |
| Owner omits `metamodel` | Use the shipped default metamodel. |
| Owner omits `deps` | Use the standard documentation dependencies. |
| `docs = None` | Retain standalone baseline settings and dependencies, and an empty exported URL. |
| Bundle has no direct sources | Continue to create no local Needs export. |
Placement and required-value checks apply when a configured local export is
analyzed. Merely composing source bundles does not require their export
configuration to be available.
### References to the owner
Use the prerequisite's `.` family: the root bundle is
`:.bundle` and the project-wide inventory is `:.needs_json`.
This proposal adds the internal helpers `.__internal__.docs_config` and
`.__internal__.sphinx_build`; it does not introduce another naming scheme
or another set of compatibility aliases.
The `docs` argument accepts a label identifying an actual `docs()` invocation,
not an arbitrary alias to its runnable target. Normalize it in the declaring
repository before deriving sibling helper labels, preserving its repository and
package. Consumers never need to spell `.__internal__.` names.
### Existing `conf.py` support
Keep the current `conf.py` support and precedence for project-wide builds. In
particular, a project's `conf.py` remains authoritative for its project name and
URL when present. The shared configuration only represents `docs()` arguments;
it does not execute or extract values from `conf.py`.
Bundle-local exports take shared fields from `docs()` arguments. Child exports
use the baseline configuration plus those fields. The root local export retains
its existing project configuration for other settings. Authors must keep shared
arguments aligned with overlapping values in a legacy `conf.py`. Supplying a URL
only in `conf.py` is insufficient for a configured local export.
Removing `conf.py` support is a future direction, outside this proposal. Python
dependencies explicitly supplied through `docs(deps = ...)` are shared with child
exports. Other Sphinx settings and external Needs imports are not automatically
inherited. Making a Python package available does not automatically enable it as
a Sphinx extension.
## Dependency graph and implementation
Macros declare targets during Bazel's loading phase. Rules consume dependency
providers during analysis. Adding fields to a child's provider makes information
available to its consumers; it does not inject a parent's settings into that
child. Dependency cycles are invalid. See the
[Bazel rules documentation](https://bazel.build/versions/8.6.0/extending/rules).
Use separate source-bundle and export targets. In this diagram, an arrow means
"depends on":
```mermaid
flowchart TD
Export["Bundle-local Needs export"] --> Adapter["Bundle Sphinx configuration adapter"]
Export --> Sources["Bundle's own sources"]
Export --> Runtime["Owner :name.__internal__.sphinx_build"]
Runtime --> Deps["Owner deps + standard documentation dependencies"]
Adapter --> Config["Owner :name.__internal__.docs_config"]
Config --> Deps
Config --> Root["Owner :name.bundle"]
Root --> Child["Child source-bundle targets"]
Sources --> Child
```
The source-bundle targets must not depend on the internal configuration or local
Needs exports. This allows the configuration target to inspect the composed source
tree without introducing a cycle. Building a local export may analyze the owner's
source-bundle graph, but must not run the owner's complete Sphinx build or other
bundles' local exports.
The public owner label is an API-level reference to the macro invocation. The
export's generated rules depend on the derived internal helpers, not on the
owner's runnable documentation target. Depending on the runnable target would
pull in unrelated build/runtime inputs and could introduce a cycle.
Internal naming means these helpers are implementation details, not that they
can have package-private visibility: local exports in other packages need to
depend on them. Give them visibility to packages in the defining repository.
Cross-repository consumers keep referencing the owner invocation defined in the
bundle's own repository; they do not select a different repository as its owner.
### Configuration and placement providers
Introduce an internal `DocsConfigInfo` provider with the project name, project
URL, optional metamodel `File`, resolved Python dependency information for `deps`,
and a mapping from bundle labels to their full placement paths. The configuration
rule depends on the root source bundle, the optional metamodel target, and the
shared Python dependencies. Its outputs must not forward the entire owner's
source tree as inputs to every local export.
Extend source-entry metadata in [bzl/bundle_rules.bzl](bzl/bundle_rules.bzl) with
the declaring bundle's Bazel label. Preserve that identity when entries are
rebased through nested bundles. Use label identity, rather than filesystem paths
or target-name strings, to look up a bundle's placement.
The configuration rule derives placements from the owner's composed entries.
For example, a parent mounted at `components` containing a child mounted at
`engine` gives that child the path `components/engine`. `attach_to` controls
navigation and does not contribute to this path. The owner's root bundle has an
empty placement path. Preserve the existing rejection of a source directory
included through more than one bundle path.
Only the selected defining project's placement determines a local export's
metadata. A downstream mount at `dependencies/example` does not prefix or replace
that publication path.
### Sphinx integration
An analysis-phase adapter consumes `DocsConfigInfo` and the exporting bundle's
label to generate Sphinx configuration inputs. Macros only wire these targets
together; they do not attempt to read providers.
Declare an owner Sphinx runtime at `.__internal__.sphinx_build`, using the
owner's `deps` plus the standard documentation dependencies. Apply the existing
dependency completion and consistency checks when declaring that runtime. Resolve
relative dependency labels in the owner's package and repository, not in each
child's package.
Let the local Needs wrapper accept this existing Sphinx executable instead of
unconditionally creating a new binary with baseline dependencies. Reuse the owner
runtime for configured local exports and the project-wide Needs build. A
standalone export continues to create its own baseline runtime. Keep executable
providers and transitive runfiles intact; serializing a list of dependency labels
into generated Python configuration would not make those packages importable.
The runtime must not depend on configuration, source-bundle composition, or any
Needs export. Export-specific inputs such as the metamodel remain action inputs.
Apply the shared settings to both child and root local exports. Preserve the
root's existing configuration for settings outside the shared fields. Keep local
exports limited to direct sources, using the existing source adapters and local
export policy. This change does not add cross-bundle Needs imports.
Represent the metamodel as a real dependency throughout the adapter and Sphinx
action. Expose it through the required action inputs and runtime data rather than
passing only a path string. Generated metamodels and files from external
repositories must work under sandboxed execution. Configuration adapters must
also preserve the source-root layout expected by the existing Sphinx rules.
## Exported URLs and compatibility
Configured bundle-local inventories export the owner URL and placement
separately:
```json
{
"project_url": "https://example.org/example",
"bundle_mount_path": "components/engine"
}
```
These are top-level inventory fields. The root local export uses an empty
`bundle_mount_path`. An unconfigured standalone export keeps an empty
`project_url` and an empty placement path.
Keep document names inside local inventories relative to their bundle. Update
both inventory import paths in
[external_needs.py](src/extensions/score_metamodel/external_needs.py) to construct
the base URL from the owner URL, the existing `main` publication directory, and
the bundle mount path, in that order:
```text
/main//.html#
```
For `details` in the example bundle, the resulting link is:
```text
https://example.org/example/main/components/engine/details.html#
```
Normalize separator boundaries without dropping the project URL's path prefix.
Do not place `main` after the bundle path or prefix document names a second time.
Generalizing the existing `main` publication convention is outside this proposal.
New consumers treat a missing `bundle_mount_path` as an empty path, preserving
existing project inventories. Older consumers do not understand the placement
field, so mount-aware local inventories require an updated consumer. This changes
the current deliberate suppression of the owner URL in local exports.
For migration, bundles belonging to subprojects select their named owner
explicitly. Standalone bundles either become part of an owner's publication tree
or set `docs = None`. Projects relying on `conf.py` supply shared values through
`docs()` as well. Existing source composition remains independent of these local
export changes.
## Acceptance scenarios
Implementation coverage should exercise behavior through the existing public
macro integration suite and focused exporter/importer tests:
- A custom metamodel is enforced in project-wide, root-local, and child-local
exports, including a failure that the shipped default would not detect.
- Generated and external metamodel files are available inside sandboxed builds.
- A Python dependency supplied only through the owner's `deps` is importable in
project-wide, root-local, and child-local Sphinx runtimes, including its
transitive dependencies and runtime data. Relative labels resolve at the owner.
- The existing dependency consistency checks still apply, and standalone exports
retain the standard dependencies.
- Direct and nested mounts produce the expected metadata and external links;
root placement, trailing URL separators, and bundle-relative document names
are covered.
- A bundle loaded through Bzlmod resolves its default configuration in its own
repository. A consuming project's settings and different mount location do
not change the defining project's export metadata.
- A named subproject owner reference works, and each `docs()` root selects its
own invocation automatically. Bundle declarations never reference internal
target names.
- Two named invocations in one package keep their shared settings, runtimes, and
placement metadata separate. Target naming and legacy aliases are covered by
the independent naming prerequisite.
- Missing configuration, missing placement, and missing explicit owner URL fail
configured local exports; `docs = None` preserves standalone behavior.
- Source-only composition still works without evaluating local export
configuration, and aggregators still have no local export.
- The dependency graph is cycle-free. Building one local export does not execute
the owner's full documentation build or sibling local exports and exports only
its own Needs.
- Both inventory import paths handle the new placement field and old inventories
without it. Existing project-wide `conf.py` precedence remains intact.
For this concept-only change, review examples against the current source and
check Markdown structure, copyright, and whitespace. Bazel builds are not needed
until the implementation changes.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the prerequisite in name_concept.md, then inspect docs.bzl and bzl/bundle_rules.bzl to understand named invocations, source-entry metadata, and bundle composition. Use the referenced Bazel rules documentation while tracing the proposed configuration provider, internal runtime, placement mapping, and Sphinx adapter. Done means configured local exports share the selected owner's settings and placement without changing standalone behavior or introducing dependency cycles.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- build-system, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100