Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ docs(
external_needs = [
"@score_process_description//:needs_json_file",
],
data = [
# These scenario BUILD files are used by literalinclude examples.
"//src/tests/docs_bzl/scenarios/nested_bundles:nested_bundle_build",
"//src/tests/docs_bzl/scenarios/data_files_runfiles:generated_data_build",
"//src/tests/docs_bzl/scenarios/external_bundle:external_bundle_build",
],
bundles = [
{
"bundle": "//src/extensions/docs:extensions",
Expand All @@ -35,6 +41,18 @@ docs(
"mount_at": "reference/metamodel",
"attach_to": "reference/index",
},
{
# Mount to show what the rendered docs look like when nested bundles are used.
"bundle": "//src/tests/docs_bzl/scenarios/nested_bundles:parent",
"mount_at": "how-to/bundles/examples/nested",
"attach_to": "how-to/bundles/examples",
},
{
# Mount to show what the rendered docs look like when data files are used.
"bundle": "//src/tests/docs_bzl/scenarios/data_files_runfiles:data_bundle",
"mount_at": "how-to/bundles/examples/generated-data",
"attach_to": "how-to/bundles/examples",
},
],
code_targets = [
"//scripts_bazel:sources",
Expand Down
28 changes: 26 additions & 2 deletions bzl/bundle_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ DocsBundleInfo = provider(
"entries": "Ordered entries, one per source directory, including its final documentation-tree location.",
"sourcelinks": "Source-code-link JSON files together with their owning repository.",
"external_runfiles": "Documentation source files from external repositories needed in runfiles.",
"data": "Non-source-tree files (e.g. genrule outputs) needed for Sphinx resolution.",
# Bundle-owned generated/supporting files. Both bundle data and
# docs(data = [...]) are build inputs; unlike host-owned docs data,
# these are resolved at this bundle's mount (for example, a generated
# index.rst).
"data": "Bundle-owned generated/supporting files resolved at the bundle's mount.",
},
)

Expand Down Expand Up @@ -145,6 +149,26 @@ def _bundle_execroot_path(runtime_path):
return "external/" + runtime_path[3:]
return runtime_path

def _pure_data_runtime_path(ctx):
"""Return a stable identity for a bundle that has no source directory.

Pure-data bundles do not use ``runtime_path`` to resolve a source tree;
their files are resolved from the manifest's ``data`` entries instead.
Give them a synthetic, label-derived identity so multiple distinct
pure-data bundles can be composed into one documentation site.
"""
# Encode the complete label identity so every target gets a stable,
# distinct synthetic path. In particular, local and external labels remain
# distinct when both are composed by one consuming project.
Comment on lines +160 to +162

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice (for future us) to add input => output

label = str(ctx.label)
encoded_label = (
label.replace("%", "%25")
.replace("@", "%40")
.replace("/", "%2F")
.replace(":", "%3A")
)
return "__data__/%s" % encoded_label

def _rebase_bundle_entry(entry, mount_at, attach_to, data):
"""Place a bundle entry below a requested documentation-tree location.

Expand Down Expand Up @@ -238,7 +262,7 @@ def _docs_bundle_impl(ctx):
elif own_data:
# Pure data bundle: create an entry so the data files appear in the manifest.
entries.append(struct(
runtime_path = "",
runtime_path = _pure_data_runtime_path(ctx),
src_root = "",
mount_at = "",
attach_to = "",
Expand Down
57 changes: 52 additions & 5 deletions docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ Easy streamlined way for S-CORE docs-as-code.

load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_venv")
load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")
load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
load(
"@sphinxdocs//sphinxdocs:sphinx.bzl",
"sphinx_build_binary",
"sphinx_docs",
)
load("@sphinxdocs//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
load(
"@score_docs_as_code//:bzl/basics.bzl",
"glob_doc_sources",
Expand Down Expand Up @@ -103,9 +108,13 @@ def docs_bundle(name, source_dir = None, data = [], entry_doc = "index", bundles
source_dir: optional directory holding this bundle's own doc sources. It is
globbed like `docs()` (same file kinds) and the contents are stored after
stripping the `source_dir` prefix. Leave it unset for a pure aggregator.
data:
Additional data dependencies for this target.
Useful for generated rst sources.
data:
Files owned by this bundle that are not discovered as documentation
sources. Use this for generated RST or other generated files that are
part of the bundle payload and belong at the bundle's eventual mount
location. Use ``docs(data = [...])`` only for project-level inputs
outside a bundle. Both forms make their files available to a build;
only bundle data travels with a mounted bundle.
entry_doc: bundle-relative docname attached when this bundle is mounted.
Defaults to `index`.
bundles: nested bundles to compose, each a dict
Expand Down Expand Up @@ -208,7 +217,12 @@ def docs(
source_dir: The source directory containing documentation files. Defaults to "docs".
project: optional project name, prefer setting this here if you can avoid having a conf.py
project_url: Optional project URL, prefer setting this here if you can avoid having a conf.py
data: Additional data files to include in the documentation build.
data: Additional files needed by the project-level documentation build.
These files are outside any bundle and have no bundle mount path. If a
file should travel with rendered or composable documentation, declare
it in a `docs_bundle` and place that bundle through `bundles` instead.
Generated documentation and assets are not staged into the workspace
source tree for `bazel run`; use a data-only `docs_bundle` for those.
deps: Additional dependencies for the documentation build.
external_needs: List of external needs targets to include in the documentation build.
scan_code: Deprecated. Explicit source files or filegroups to scan for source
Expand All @@ -230,6 +244,12 @@ def docs(
}.
Note: a bundle label may also point at another module's auto-exposed
bundle, e.g. "@score_process_description//:docs_bundle".

The short rule is about ownership and placement, not build availability:
use a bundle for anything that should travel as one portable mount. A
bundle may be data-only when its deliverable is generated/supporting data
rather than source RST. Use ``docs(data = [...])`` only for project-level
inputs that do not belong to a bundle mount.
"""
# HINT: keep documentation sync docs/reference/bazel_macros.rst

Expand Down Expand Up @@ -257,6 +277,32 @@ def docs(
# list-valued attributes such as ``data`` and ``tools``.
metamodel_label = [metamodel] if metamodel else []

data_library_label_for_sphinx_docs = []
if data:
# ``docs_bundle`` can carry data, including as a pure-data bundle. That
# data belongs to the bundle payload and is resolved at its eventual
# mount. These ``docs(data = [...])`` inputs are intentionally
# project-level instead: they support the project build or its
# literalinclude examples and are not assigned to a bundle mount. Both
# kinds of data are build inputs; without the staging below, project-
# level inputs would
# remain only execution inputs for Sphinx's tools rather than files
# below Sphinx's source directory, where standard ``literalinclude``
# looks for them.
#
# ``sphinx_docs_library`` is the generic rules_python/rules_sphinxdocs
# mechanism for adding such files to the sandboxed needs source tree.
# Preserve their workspace-relative paths so one ordinary
# literalinclude works in that Sphinx action. The interactive run
# target reads the workspace source tree directly; generated files
# belong in a data-only docs_bundle instead.
sphinx_docs_library(
name = "_docs_data",
srcs = data,
strip_prefix = "",
)
data_library_label_for_sphinx_docs = [":_docs_data"]

mounts_manifest_label = []
if bundles:
mounts_bundle = create_bundle(
Expand Down Expand Up @@ -409,6 +455,7 @@ def docs(
sphinx_docs(
name = "needs_json",
srcs = [":docs_bundle"],
deps = data_library_label_for_sphinx_docs,
config = sphinx_config,
extra_opts = [
"-W",
Expand Down
89 changes: 89 additions & 0 deletions docs/how-to/bundles/examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

Bundle examples
===============

This page is a catalog of focused bundle patterns. Start with
:doc:`index` for the basic workflow; use these examples when documentation is
split across bundles, generated during the build, or published by another
Bazel module. Each example shows the bundle declaration, its placement in the
consuming project, and the resulting location in the documentation output.

Nest bundles
------------

Use nested bundles when one bundle contains another. The child bundle chooses
its entry page, the parent chooses the child's relative location, and the
consuming project chooses where the assembled bundle appears.

The tested parent bundle composes the child like this:

.. literalinclude:: ../../../src/tests/docs_bzl/scenarios/nested_bundles/BUILD
:language: starlark
:start-after: BEGIN docs-bundle-howto: parent-composition
:end-before: END docs-bundle-howto: parent-composition

See the `complete nested-bundles fixture on GitHub
<https://github.com/eclipse-score/docs-as-code/tree/main/src/tests/docs_bzl/scenarios/nested_bundles>`_.

If the parent is mounted at ``guides/example``, its page is rendered at
``guides/example/index.html`` and the child landing page at
``guides/example/child/landing.html``. The consuming project can change
``guides/example`` without changing either bundle.

Mount generated documentation
-----------------------------

Use a data-only bundle when a build action produces the documentation rather
than a source-tree ``.rst`` file. It has no ``source_dir``; its generated files
are the bundle's complete payload. You can generate and mount a page like this:

.. literalinclude:: ../../../src/tests/docs_bzl/scenarios/data_files_runfiles/BUILD
:language: starlark
:start-after: BEGIN docs-bundle-howto: generated-data
:end-before: END docs-bundle-howto: generated-data

See the `complete generated-data fixture on GitHub
<https://github.com/eclipse-score/docs-as-code/tree/main/src/tests/docs_bzl/scenarios/data_files_runfiles>`_.

The generated page is rendered at ``data_test/index.html`` and is added to the
consuming project's index page's toctree. Although this bundle is data-only,
it is mounted and navigated in exactly the same way as a bundle with source
files.

Mount documentation from another module
----------------------------------------

Every project using ``docs()`` exposes its own documentation as a
``:docs_bundle`` target. The external-bundle test fixture mounts that target
like this:

.. literalinclude:: ../../../src/tests/docs_bzl/scenarios/external_bundle/BUILD
:language: starlark
:start-after: BEGIN docs-bundle-howto: external-bundle
:end-before: END docs-bundle-howto: external-bundle

See the `complete external-bundle fixture on GitHub
<https://github.com/eclipse-score/docs-as-code/tree/main/src/tests/docs_bzl/scenarios/external_bundle>`_.

The external bundle's entry page is rendered at ``process/index.html`` and is
added to the consuming project's index by ``attach_to``. Mounted sources also
bring their Need directives with them, so do not import the same module's
``needs_json`` separately.

.. seealso::

:ref:`howto_mount_external_sources` for the full mount reference and
:ref:`docs_bazel-macros` for the ``docs`` and ``docs_bundle`` attributes.
121 changes: 121 additions & 0 deletions docs/how-to/bundles/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
..
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

.. _howto_mount_external_sources:

How to use bundles
==================

This page is the getting-started guide for documentation bundles: define a
bundle, mount it in a project, compose it with other bundles, and consume a
bundle from another module. For focused patterns and test-backed examples,
see :doc:`examples`.

Declare a bundle
----------------

Create a ``docs_bundle`` next to the source directory that it exports:

.. code-block:: starlark

# src/BUILD
load("//:docs.bzl", "docs_bundle")

docs_bundle(
name = "docs_dir",
source_dir = "docs",
visibility = ["//visibility:public"],
)

``source_dir`` is the root of the mounted tree. Its supported documentation
files are collected like those of ``docs()``; for example,
``src/docs/overview.rst`` becomes ``overview`` within the bundle.
``entry_doc`` names the bundle-relative page used for navigation when the
bundle is mounted.

Mount a bundle in a project
---------------------------

Pass a placement dictionary to ``docs(bundles = [...])``:

.. code-block:: starlark

load("//:docs.bzl", "docs")

docs(
bundles = [
{
"bundle": "//src:docs_dir",
"mount_at": "internals/code_docs",
},
],
source_dir = "docs",
)

``bundle`` identifies the ``docs_bundle`` target and ``mount_at`` selects the
docname prefix in this project. In this example, ``overview.rst`` is available
as ``internals/code_docs/overview``.

Every mount adds the bundle's configured entry document to a host toctree. By
default that is the ``index`` beside ``mount_at``; ``attach_to`` overrides the
host document whose first toctree receives the entry. The entry itself belongs
to the ``docs_bundle`` and defaults to ``index``.

The configuration has two separate responsibilities:

.. plantuml::

@startuml
left to right direction
rectangle "docs_bundle\ncontent" as bundle
rectangle "docs(bundles = [...])\nplacement" as docs
rectangle "Rendered docs" as output
bundle --> docs : select bundle
docs --> output : mount_at / attach_to
@enduml

For nested composition, generated bundle data, and bundles published by
another Bazel module, see :doc:`examples`.

Refresh the generated project configuration
-------------------------------------------

After changing ``bundles``, run either command once:

.. code-block:: console

$ bazel run //:docs
$ bazel run //:docs_check

Both commands refresh the repository-root ``ubproject.toml`` used by IDE tools.
``bazel build //:needs_json`` checks the sandboxed needs build without creating
that developer-facing file.

Further Examples
----------------

The :doc:`examples` page shows nested composition, generated bundle data, and
the declaration for a bundle exported by another Bazel module.

.. toctree::
:maxdepth: 1
:hidden:

examples

Further reading
---------------

* :ref:`docs_concept_mounts` — bundle and mount semantics.
* `sphinx-mounts documentation <https://sphinx-mounts.useblocks.com/>`_ — the underlying mechanism.
Loading
Loading