From c6185ac4f7e4e9db6acb37523fd1017627b90a9c Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Tue, 25 Aug 2026 19:15:41 +0200 Subject: [PATCH 1/2] fix: avoid duplicate needs from nested docs bundles --- bzl/bundle_rules.bzl | 23 +++++ docs.bzl | 11 ++- src/tests/docs_bzl/README.md | 1 + .../subdirectory_bundle/consumer/BUILD | 26 ++++++ .../subdirectory_bundle/consumer/docs/conf.py | 17 ++++ .../consumer/docs/index.rst | 23 +++++ .../subdirectory_bundle/producer/BUILD | 25 ++++++ .../subdirectory_bundle/producer/docs/conf.py | 17 ++++ .../producer/docs/index.rst | 22 +++++ .../producer/embedded/BUILD | 20 +++++ .../producer/embedded/content/index.rst | 22 +++++ .../docs_bzl/test_subdirectory_bundle.py | 86 +++++++++++++++++++ 12 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/index.rst create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/index.rst create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/BUILD create mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/embedded/content/index.rst create mode 100644 src/tests/docs_bzl/test_subdirectory_bundle.py diff --git a/bzl/bundle_rules.bzl b/bzl/bundle_rules.bzl index ace5d796d..6019b288e 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.", "data": "Non-source-tree files (e.g. genrule outputs) needed for Sphinx resolution.", @@ -288,6 +289,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, @@ -327,6 +329,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 eafe15481..75e501ef2 100644 --- a/docs.bzl +++ b/docs.bzl @@ -52,6 +52,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", @@ -299,6 +300,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", @@ -408,7 +414,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], config = sphinx_config, extra_opts = [ "-W", 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..280660c65 --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD @@ -0,0 +1,26 @@ +# ******************************************************************************* +# 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", + 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/conf.py b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py new file mode 100644 index 000000000..0b6dd5d17 --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +project = "Subdirectory Bundle Consumer" +project_url = "https://example.invalid/subdirectory-bundle-consumer" +extensions = ["score_sphinx_bundle"] +required_in_id = ["consumer", "producer", "embedded"] 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..dfdbf7e26 --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD @@ -0,0 +1,25 @@ +# ******************************************************************************* +# 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", + 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/conf.py b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py new file mode 100644 index 000000000..0e50d1e5a --- /dev/null +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py @@ -0,0 +1,17 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +project = "Subdirectory Bundle Producer" +project_url = "https://example.invalid/subdirectory-bundle-producer" +extensions = ["score_sphinx_bundle"] +required_in_id = ["producer", "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") From 91680fc0208d90ab1e4f99bfd511666f2d8ba098 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Wed, 26 Aug 2026 10:05:25 +0200 Subject: [PATCH 2/2] drop conf.py in new tests --- .../subdirectory_bundle/consumer/BUILD | 2 ++ .../subdirectory_bundle/consumer/docs/conf.py | 17 ----------------- .../subdirectory_bundle/producer/BUILD | 2 ++ .../subdirectory_bundle/producer/docs/conf.py | 17 ----------------- 4 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py delete mode 100644 src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD index 280660c65..c5323288c 100644 --- a/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/BUILD @@ -18,6 +18,8 @@ load("//:docs.bzl", "docs") # 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", diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py b/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py deleted file mode 100644 index 0b6dd5d17..000000000 --- a/src/tests/docs_bzl/scenarios/subdirectory_bundle/consumer/docs/conf.py +++ /dev/null @@ -1,17 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* - -project = "Subdirectory Bundle Consumer" -project_url = "https://example.invalid/subdirectory-bundle-consumer" -extensions = ["score_sphinx_bundle"] -required_in_id = ["consumer", "producer", "embedded"] diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD index dfdbf7e26..cee570dfb 100644 --- a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD +++ b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/BUILD @@ -17,6 +17,8 @@ load("//:docs.bzl", "docs") # 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", diff --git a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py b/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py deleted file mode 100644 index 0e50d1e5a..000000000 --- a/src/tests/docs_bzl/scenarios/subdirectory_bundle/producer/docs/conf.py +++ /dev/null @@ -1,17 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* - -project = "Subdirectory Bundle Producer" -project_url = "https://example.invalid/subdirectory-bundle-producer" -extensions = ["score_sphinx_bundle"] -required_in_id = ["producer", "embedded"]