diff --git a/bzl/bundle_rules.bzl b/bzl/bundle_rules.bzl index 1b0554419..7145741f4 100644 --- a/bzl/bundle_rules.bzl +++ b/bzl/bundle_rules.bzl @@ -62,6 +62,7 @@ DocsBundleInfo = provider( doc = "A documentation bundle with its source and placement metadata.", fields = { "entries": "Ordered entries, one per source directory, including its final documentation-tree location.", + "own_source_files": "This bundle's direct source files, excluding nested bundles.", "sourcelinks": "Source-code-link JSON files together with their owning repository.", "external_runfiles": "Documentation source files from external repositories needed in runfiles.", # Bundle-owned generated/supporting files. Both bundle data and @@ -312,6 +313,7 @@ def _docs_bundle_impl(ctx): DefaultInfo(files = depset(transitive = [all_source_files, all_data])), DocsBundleInfo( entries = entries, + own_source_files = depset(direct = own_source_files), sourcelinks = sourcelinks, external_runfiles = external_runfiles, data = all_data, @@ -351,6 +353,27 @@ def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "", ) return ":" + name +def _bundle_source_files_impl(ctx): + """Expose only a bundle's direct sources as a Sphinx source tree.""" + return [DefaultInfo(files = ctx.attr.bundle[DocsBundleInfo].own_source_files)] + +_bundle_source_files = rule( + implementation = _bundle_source_files_impl, + attrs = { + "bundle": attr.label(providers = [DocsBundleInfo]), + }, + doc = "Exposes direct bundle sources without nested bundle sources.", +) + +def bundle_source_files(name, bundle, visibility = None): + """Create a target containing only the direct sources of a bundle.""" + _bundle_source_files( + name = name, + bundle = bundle, + visibility = visibility, + ) + return ":" + name + def _external_docs_runfiles_impl(ctx): """Expose external documentation sources needed under ``bazel run``.""" bundle = ctx.attr.bundle[DocsBundleInfo] diff --git a/docs.bzl b/docs.bzl index 1cd63753c..c08abf77d 100644 --- a/docs.bzl +++ b/docs.bzl @@ -57,6 +57,7 @@ load( load( "@score_docs_as_code//:bzl/bundle_rules.bzl", "create_bundle", + "bundle_source_files", "merge_bundle_sourcelinks", "external_docs_runfiles", "generate_code_target_sourcelinks", @@ -345,6 +346,11 @@ def docs( code_targets = code_targets, visibility = ["//visibility:public"], ) + sphinx_sources = bundle_source_files( + name = "_docs_sphinx_sources", + bundle = ":docs_bundle", + visibility = ["//visibility:private"], + ) merge_bundle_sourcelinks( name = "sourcelinks_json", bundle = ":docs_bundle", @@ -454,7 +460,10 @@ def docs( sphinx_docs( name = "needs_json", - srcs = [":docs_bundle"], + # Nested bundle sources are mounted by score_mounts. Passing the + # complete bundle as srcs would also expose those files as raw Sphinx + # sources and make every nested need appear twice. + srcs = [sphinx_sources], deps = data_library_label_for_sphinx_docs, config = sphinx_config, extra_opts = [ diff --git a/src/tests/docs_bzl/README.md b/src/tests/docs_bzl/README.md index cb6c7b313..b270555a9 100644 --- a/src/tests/docs_bzl/README.md +++ b/src/tests/docs_bzl/README.md @@ -20,6 +20,7 @@ docs_bzl/ │ ├── external_needs/ │ ├── metamodel_violation/ │ ├── nested_bundles/ +│ ├── subdirectory_bundle/ │ ├── external_bundle/ │ ├── local_version_mismatch/ │ └── invalid_bundle_placements/ diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD new file mode 100644 index 000000000..c5323288c --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD @@ -0,0 +1,28 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +# Consume the producer's complete public docs bundle. This exercises the +# transitive bundle assembled by producer/docs(), including its subdirectory +# bundle, without adding the producer's needs_json separately. +docs( + source_dir = "docs", + project = "Subdirectory Bundle Consumer", + project_url = "https://example.invalid/subdirectory-bundle-consumer", + test_sources = ["src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer"], + bundles = [{ + "bundle": "//src/tests/docs_bzl/scenarios/subdirectory_bundle/producer:docs_bundle", + "mount_at": "producer", + }], +) diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/index.rst b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/index.rst new file mode 100644 index 000000000..54a0108db --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/index.rst @@ -0,0 +1,23 @@ +.. + ******************************************************************************* + 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 + ******************************************************************************* + +Consumer documentation +====================== + +.. gd_req:: Consumer requirement + :id: gd_req__consumer + :version: 1 + + The consumer also has a local requirement, so its needs output can be + checked for exactly one copy of each mounted requirement. diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD new file mode 100644 index 000000000..cee570dfb --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD @@ -0,0 +1,27 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +# The embedded bundle lives in a subdirectory package alongside the docs source. +# The parent docs() must mount it explicitly because it is a separate package. +docs( + source_dir = "docs", + project = "Subdirectory Bundle Producer", + project_url = "https://example.invalid/subdirectory-bundle-producer", + test_sources = ["src/tests/docs_bzl/scenarios/subdirectory_bundle/producer"], + bundles = [{ + "bundle": "//src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded:docs_bundle", + "mount_at": "embedded", + }], +) diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/index.rst b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/index.rst new file mode 100644 index 000000000..20ca12f0a --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/index.rst @@ -0,0 +1,22 @@ +.. + ******************************************************************************* + 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 + ******************************************************************************* + +Producer documentation +====================== + +.. gd_req:: Producer root requirement + :id: gd_req__producer_root + :version: 1 + + The producer's regular documentation is part of its public docs bundle. diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/BUILD b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/BUILD new file mode 100644 index 000000000..f19e1e6ff --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/BUILD @@ -0,0 +1,20 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("//:docs.bzl", "docs_bundle") + +docs_bundle( + name = "docs_bundle", + source_dir = "content", + visibility = ["//visibility:public"], +) diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/content/index.rst b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/content/index.rst new file mode 100644 index 000000000..ffd7b2859 --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/content/index.rst @@ -0,0 +1,22 @@ +.. + ******************************************************************************* + 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 + ******************************************************************************* + +Embedded documentation +====================== + +.. gd_req:: Embedded requirement + :id: gd_req__embedded + :version: 1 + + The embedded documentation is supplied by a docs_bundle in a subdirectory. diff --git a/src/tests/docs_bzl/test_subdirectory_bundle.py b/src/tests/docs_bzl/test_subdirectory_bundle.py new file mode 100644 index 000000000..bf26263ab --- /dev/null +++ b/src/tests/docs_bzl/test_subdirectory_bundle.py @@ -0,0 +1,86 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Coverage for docs() with a docs_bundle in a nested Bazel package.""" + +from src.tests.docs_bzl.helpers import load_needs, run_bazel, run_package + +PRODUCER = "//src/tests/docs_bzl/scenarios/subdirectory_bundle/producer" +CONSUMER = "//src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer" + + +def test_docs_targets_build_with_a_bundle_in_a_subdirectory(): + targets = [ + "docs", + "docs_check", + "docs_link_check", + "live_preview", + "ide_support", + "docs_bundle", + "sourcelinks_json", + "needs_json", + "metrics_json", + "needs_json_file", + "traceability_gate", + ] + run_bazel( + [ + "build", + *[ + f"{package}:{target}" + for package in (PRODUCER, CONSUMER) + for target in targets + ], + ] + ) + + +def test_producer_needs_include_subdirectory_bundle_once(): + result = run_package( + "build", "scenarios/subdirectory_bundle/producer", ":needs_json" + ) + assert result.artifacts is not None + + needs = load_needs(result.artifacts["needs.json"]) + assert set(needs) == { + "gd_req__producer_root", + "gd_req__embedded", + } + + +def test_consumer_can_render_and_check_transitive_bundle_without_duplicate_needs(): + result = run_package( + "build", "scenarios/subdirectory_bundle/consumer", ":needs_json" + ) + assert result.artifacts is not None + needs = load_needs(result.artifacts["needs.json"]) + assert set(needs) == { + "gd_req__producer_root", + "gd_req__embedded", + "gd_req__consumer", + } + + producer_result = run_package( + "run", "scenarios/subdirectory_bundle/producer", ":docs" + ) + assert (producer_result.build_dir / "embedded" / "index.html").is_file() + + consumer_result = run_package( + "run", "scenarios/subdirectory_bundle/consumer", ":docs" + ) + assert (consumer_result.build_dir / "producer" / "index.html").is_file() + assert ( + consumer_result.build_dir / "producer" / "embedded" / "index.html" + ).is_file() + + run_package("run", "scenarios/subdirectory_bundle/consumer", ":docs_check") + run_package("run", "scenarios/subdirectory_bundle/consumer", ":docs_link_check")