diff --git a/docs.bzl b/docs.bzl index 85e08f800..ad9d76ee6 100644 --- a/docs.bzl +++ b/docs.bzl @@ -478,11 +478,12 @@ def _declare_bundle_local_needs( sphinx_build_deps = _sphinx_runtime_deps(deps) needs_local = _bundle_internal_target(name, "needs_local") + needs_local_builder = _bundle_internal_target(name, "needs_local_builder") # The source provider and local manifest describe the same owner-only # document set. Pass both to Sphinx so mounted descendants are neither # loaded as local sources nor assigned to this bundle's Needs export. _needs_sphinx_docs( - name = needs_local, + name = needs_local_builder, bundle = source_bundle, config = config, sphinx_build_deps = sphinx_build_deps, @@ -496,7 +497,24 @@ def _declare_bundle_local_needs( score_sourcelinks_json = sourcelinks_json, score_source_code_linker_plain_links = "1", mounts_manifest = mounts_manifest, + # Sphinx writes a directory containing several builder outputs. Keep + # that implementation output private; external consumers need only + # the extracted needs.json file below. + visibility = ["//visibility:private"], + ) + + # A Bazel directory artifact cannot also expose one of its children as a + # separate output of the same action. Keep Sphinx's directory output as an + # intermediate and publish just needs.json through the stable + # needs_local target. External-Needs loading then depends on a file artifact + # instead of reconstructing Sphinx's private output layout. + native.genrule( + name = needs_local, + srcs = [":" + needs_local_builder], + outs = [needs_local + "/needs.json"], + cmd = "cp $(execpath :" + needs_local_builder + ")/needs.json $@", visibility = visibility, + tags = ["manual"], ) def docs_bundle( diff --git a/docs/reference/bazel_macros.rst b/docs/reference/bazel_macros.rst index e18330255..26b33f054 100644 --- a/docs/reference/bazel_macros.rst +++ b/docs/reference/bazel_macros.rst @@ -250,11 +250,11 @@ Signature: ``docs_bundle(name, source_dir = None, srcs = [], data = [], entry_do - ``needs_local`` (internal target) A source-bearing bundle creates ``.__internal__.needs_local`` with the - Needs declared by its own sources. The standalone build is intentionally - self-contained in this version: references to Needs defined outside the - bundle remain unresolved and fail strict builds. Cross-bundle imports and - merged exports are planned for a later change. Data-only bundles do not - create a Needs target. + ``needs.json`` file for Needs declared by its own sources. The standalone + build is intentionally self-contained in this version: references to Needs + defined outside the bundle remain unresolved and fail strict builds. + Cross-bundle imports and merged exports are planned for a later change. + Data-only bundles do not create a Needs target. .. note:: diff --git a/src/helper_lib/external_needs.py b/src/helper_lib/external_needs.py index dccc5e6cd..a50797446 100644 --- a/src/helper_lib/external_needs.py +++ b/src/helper_lib/external_needs.py @@ -93,9 +93,9 @@ def external_needs_source_path( ) -> Path: """Find the inventory JSON emitted by the selected Bazel target. - Public `needs_json` and private bundle-local exports are directory outputs - containing `_build/needs/needs.json`. The `needs_json_file` target points - directly at the inventory file. + The public `needs_json` target is a directory output containing + `_build/needs/needs.json`. The `needs_json_file` target and private + bundle-local exports point directly at JSON file artifacts. """ if runfiles_dir is None: raise ValueError("An external needs source has no runfiles root.") @@ -105,7 +105,9 @@ def external_needs_source_path( elif source.target == "needs_json_file": suffix = ("needs.json",) elif source.target.endswith(".__internal__.needs_local"): - suffix = (source.target, "_build", "needs", "needs.json") + # The genrule exposes `/needs.json`. This target-scoped path is + # independent of the private Sphinx builder's output directory layout. + suffix = (source.target, "needs.json") else: raise ValueError(f"Unsupported external needs target: {source.target}") return external_needs_runfiles_path(runfiles_dir, source, *suffix) diff --git a/src/tests/docs_bzl/expected_outputs.py b/src/tests/docs_bzl/expected_outputs.py index 6039bf46b..6e75bc30e 100644 --- a/src/tests/docs_bzl/expected_outputs.py +++ b/src/tests/docs_bzl/expected_outputs.py @@ -77,25 +77,25 @@ class ExpectedTarget: label=":docs_bundle.__internal__.needs_local", command="build", output_kind="file", - output_path="docs_bundle.__internal__.needs_local/_build/needs/needs.json", + output_path="docs_bundle.__internal__.needs_local/needs.json", ), "root_needs_local": ExpectedTarget( label=":docs.__internal__.needs_local", command="build", output_kind="file", - output_path="docs.__internal__.needs_local/_build/needs/needs.json", + output_path="docs.__internal__.needs_local/needs.json", ), "data_bundle_needs": ExpectedTarget( label=":data_bundle.__internal__.needs_local", command="build", - output_kind="directory", - output_path="data_bundle.__internal__.needs_local/_build/needs", + output_kind="file", + output_path="data_bundle.__internal__.needs_local/needs.json", ), "isolated_source_bundle_needs": ExpectedTarget( label=":isolated_source_bundle.__internal__.needs_local", command="build", - output_kind="directory", - output_path="isolated_source_bundle.__internal__.needs_local/_build/needs", + output_kind="file", + output_path="isolated_source_bundle.__internal__.needs_local/needs.json", ), "sourcelinks_json": ExpectedTarget( label=":sourcelinks_json", diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/data_bundle_needs/needs.json b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/data_bundle_needs.json similarity index 100% rename from src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/data_bundle_needs/needs.json rename to src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/data_bundle_needs.json diff --git a/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/isolated_source_bundle_needs/needs.json b/src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/isolated_source_bundle_needs.json similarity index 100% rename from src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/isolated_source_bundle_needs/needs.json rename to src/tests/docs_bzl/scenarios/data_files_runfiles/_expected/isolated_source_bundle_needs.json diff --git a/src/tests/docs_bzl/test_root_docs_config.py b/src/tests/docs_bzl/test_root_docs_config.py index 97d340d11..96e91f5a5 100644 --- a/src/tests/docs_bzl/test_root_docs_config.py +++ b/src/tests/docs_bzl/test_root_docs_config.py @@ -29,7 +29,7 @@ def test_child_bundle_uses_root_docs_config_without_a_child_conf_py(): needs_json = built_output( "scenarios/root_docs_config", - "component.__internal__.needs_local/_build/needs/needs.json", + "component.__internal__.needs_local/needs.json", ) needs = load_needs(needs_json) metadata = load_needs_json(needs_json)