diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 00d5a237..f49229b6 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -1807,6 +1807,9 @@ def dependable_element( dependable_element's `deps` are resolved against `_index` (see `processed_deps`), not against `` itself. : Main dependable element target (sphinx_module) with HTML documentation + .serve: Alias to _doc.serve, a binary that locally serves + 's HTML output for previewing docs during development + (`bazel run //:.serve`). _needs: Sphinx-needs JSON target (created by sphinx_module for cross-referencing) """ @@ -1875,3 +1878,14 @@ def dependable_element( testonly = testonly, visibility = ["//visibility:public"], ) + + # Step 5: Alias the internal "_doc.serve" preview binary to + # ".serve" so callers can preview docs without knowing about the + # internal _doc split (mirrors the sphinx_module_dep facade in Step 3). + native.alias( + name = name + ".serve", + actual = ":" + name + "_doc.serve", + tags = ["manual"], + testonly = testonly, + visibility = kwargs.get("visibility"), + ) diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index 269ca96e..313c9396 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -10,15 +10,21 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +# Copyright 2023 The Bazel Authors. All rights reserved. +# https://github.com/bazel-contrib/rules_python/blob/release/1.8/sphinxdocs/private/sphinx.bzl + # ====================================================================================== # Helpers # ====================================================================================== load("@bazel_skylib//lib:paths.bzl", "paths") +load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") load("@rules_python//sphinxdocs/private:sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo") load("//bazel/rules/rules_score:providers.bzl", "FilteredExecpathInfo", "SphinxIndexFileInfo", "SphinxModuleInfo", "SphinxNeedsInfo") load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") +_SPHINX_SERVE_MAIN_SRC = Label("@rules_python//sphinxdocs/private:sphinx_server.py") + # Maps the //bazel/rules/rules_score:verbosity build setting (see # verbosity.bzl) to the sphinx-build CLI flags that achieve it. Lives here in # Starlark rather than in a wrapper script: the Sphinx build binary is @@ -628,6 +634,20 @@ _score_html = rule( # ====================================================================================== # Rule wrappers # ====================================================================================== +def _copy_propagating_kwargs(from_kwargs): + """Return the subset of macro kwargs that must stay consistent across + sibling targets with a dependency relationship. + + Deliberately excludes `visibility`: callers of this helper want their + generated sub-target to NOT inherit the macro's own (often public) + visibility. + """ + into_kwargs = {} + for attr in ("testonly", "tags", "compatible_with", "restricted_to", "target_compatible_with"): + if attr in from_kwargs: + into_kwargs[attr] = from_kwargs[attr] + return into_kwargs + def sphinx_module( name, srcs, @@ -646,6 +666,14 @@ def sphinx_module( transitive dependency collection. Each dependency's HTML is copied into a / subdirectory of the merged site for intersphinx/sphinx-needs cross-referencing. + + Generates targets: + * ``: The merged HTML site (this module's own HTML plus every + transitive dependency's HTML, each under a `/` subdirectory). + * `.serve`: A binary that locally serves ``'s HTML output, + for previewing docs during development (`bazel run //:.serve`). + * `_needs`: This module's `needs.json` build (see SphinxNeedsInfo). + Args: name: Name of the target srcs: List of source files (.rst, .md) with index file first @@ -679,9 +707,9 @@ def sphinx_module( # conf.py generation is a private implementation detail consumed only by # the sibling _score_needs/_score_html targets below (same package) -- - # must not inherit the macro's own (often public) visibility via kwargs. - conf_kwargs = dict(kwargs) - conf_kwargs.pop("visibility", None) + # _copy_propagating_kwargs both drops visibility and narrows to the + # attrs that must actually stay consistent across the two. + conf_kwargs = _copy_propagating_kwargs(kwargs) _score_conf( name = name + "_needs_conf", @@ -727,3 +755,15 @@ def sphinx_module( testonly = testonly, **kwargs ) + + serve_kwargs = _copy_propagating_kwargs(kwargs) + serve_kwargs["tags"] = list(serve_kwargs.get("tags") or []) + ["manual"] + py_binary( + name = name + ".serve", + srcs = [_SPHINX_SERVE_MAIN_SRC], + main = _SPHINX_SERVE_MAIN_SRC, + data = [name], + args = ["$(execpath {})".format(name)], + testonly = testonly, + **serve_kwargs + ) diff --git a/bazel/rules/rules_score/test/BUILD b/bazel/rules/rules_score/test/BUILD index 693f6ccb..c8084df0 100644 --- a/bazel/rules/rules_score/test/BUILD +++ b/bazel/rules/rules_score/test/BUILD @@ -43,6 +43,7 @@ load( "needs_generation_test", "needs_transitive_test", "providers_test", + "serve_target_test", "sphinx_module_test_suite", ) load( @@ -95,6 +96,7 @@ load( "seooc_index_generation_test", "seooc_multi_index_files_exist_test", "seooc_needs_provider_test", + "seooc_serve_alias_test", "seooc_sphinx_entry_point_is_root_index_test", "seooc_sphinx_module_generated_test", ) @@ -969,6 +971,11 @@ explicit_config_test( target_under_test = ":module_a_lib", ) +serve_target_test( + name = "serve_target_test", + target_under_test = ":module_a_lib.serve", +) + # ============================================================================ # HTML Content Validation Tests # ============================================================================ @@ -1144,6 +1151,12 @@ seooc_sphinx_module_generated_test( target_under_test = ":seooc_test_lib", ) +# Test that the .serve alias resolves to a runnable preview binary +seooc_serve_alias_test( + name = "seooc_tests_serve_alias", + target_under_test = ":seooc_test_lib.serve", +) + # Test that needs provider exists for cross-referencing seooc_needs_provider_test( name = "seooc_tests_needs_provider", @@ -1273,6 +1286,7 @@ test_suite( ":seooc_tests_index_generation", ":seooc_tests_multi_index_files_exist", ":seooc_tests_needs_provider", + ":seooc_tests_serve_alias", ":seooc_tests_sphinx_entry_point_is_root_index", ":seooc_tests_sphinx_module_generated", ], diff --git a/bazel/rules/rules_score/test/html_generation_test.bzl b/bazel/rules/rules_score/test/html_generation_test.bzl index 922361b2..f718909f 100644 --- a/bazel/rules/rules_score/test/html_generation_test.bzl +++ b/bazel/rules/rules_score/test/html_generation_test.bzl @@ -195,6 +195,25 @@ def _explicit_config_test_impl(ctx): explicit_config_test = analysistest.make(_explicit_config_test_impl) +# ============================================================================ +# Serve Target Tests +# ============================================================================ + +def _serve_target_test_impl(ctx): + """Test that sphinx_module generates a runnable .serve py_binary.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + asserts.true( + env, + target_under_test[DefaultInfo].files_to_run.executable != None, + ".serve should be an executable py_binary", + ) + + return analysistest.end(env) + +serve_target_test = analysistest.make(_serve_target_test_impl) + # ============================================================================ # Test Suite # ============================================================================ @@ -228,5 +247,8 @@ def sphinx_module_test_suite(name): # Config generation ":auto_config_generation_test", ":explicit_config_test", + + # Local preview binary + ":serve_target_test", ], ) diff --git a/bazel/rules/rules_score/test/seooc_test.bzl b/bazel/rules/rules_score/test/seooc_test.bzl index 081de8b2..7cf62661 100644 --- a/bazel/rules/rules_score/test/seooc_test.bzl +++ b/bazel/rules/rules_score/test/seooc_test.bzl @@ -98,6 +98,23 @@ seooc_sphinx_module_generated_test = analysistest.make( impl = _seooc_sphinx_module_generated_test_impl, ) +def _seooc_serve_alias_test_impl(ctx): + """Test that dependable_element aliases _doc.serve to .serve.""" + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + + asserts.true( + env, + target_under_test[DefaultInfo].files_to_run.executable != None, + "Expected dependable_element's .serve alias to resolve to a runnable py_binary", + ) + + return analysistest.end(env) + +seooc_serve_alias_test = analysistest.make( + impl = _seooc_serve_alias_test_impl, +) + def _seooc_needs_provider_test_impl(ctx): """Test that dependable_element generates needs provider for cross-referencing.""" env = analysistest.begin(ctx)