From a2b7801bc452671f4f3d7d6c6723490e02292946 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Tue, 4 Aug 2026 01:29:14 +0200 Subject: [PATCH 1/3] feat: optional conf.py --- BUILD | 5 +- .../docs/conf.py => default_conf.py.tpl | 9 ++- docs.bzl | 68 ++++++++++++++++++- docs/how-to/setup.md | 15 +++- docs/reference/bazel_macros.rst | 10 ++- src/incremental.py | 9 +++ src/tests/docs_bzl/scenarios/basic_docs/BUILD | 2 + .../scenarios/missing_docs_config/BUILD | 12 ++++ .../missing_docs_config/docs/index.rst | 16 +++++ src/tests/docs_bzl/test_basic_docs.py | 4 ++ .../docs_bzl/test_missing_docs_config.py | 18 +++++ 11 files changed, 159 insertions(+), 9 deletions(-) rename src/tests/docs_bzl/scenarios/basic_docs/docs/conf.py => default_conf.py.tpl (69%) create mode 100644 src/tests/docs_bzl/scenarios/missing_docs_config/BUILD create mode 100644 src/tests/docs_bzl/scenarios/missing_docs_config/docs/index.rst create mode 100644 src/tests/docs_bzl/test_missing_docs_config.py diff --git a/BUILD b/BUILD index 38137587b..030eb112d 100644 --- a/BUILD +++ b/BUILD @@ -14,7 +14,10 @@ load("//:docs.bzl", "docs") package(default_visibility = ["//visibility:public"]) -exports_files(["pyproject.toml"]) +exports_files([ + "default_conf.py.tpl", + "pyproject.toml", +]) docs( external_needs = [ diff --git a/src/tests/docs_bzl/scenarios/basic_docs/docs/conf.py b/default_conf.py.tpl similarity index 69% rename from src/tests/docs_bzl/scenarios/basic_docs/docs/conf.py rename to default_conf.py.tpl index 9b0fd6e88..bc2806c01 100644 --- a/src/tests/docs_bzl/scenarios/basic_docs/docs/conf.py +++ b/default_conf.py.tpl @@ -10,7 +10,12 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +# Default Sphinx configuration emitted by the ``docs()`` macro. +# SCORE Docs-as-Code owns these baseline settings. Projects needing further +# Sphinx configuration can provide their own conf.py instead. -project = "Basic Test" -project_url = "https://github.com/eclipse-score/docs-as-code" +project = {PROJECT} +project_url = {PROJECT_URL} + +version = "0.1" extensions = ["score_sphinx_bundle"] diff --git a/docs.bzl b/docs.bzl index 394351132..f7d9cadd3 100644 --- a/docs.bzl +++ b/docs.bzl @@ -60,6 +60,31 @@ load( "create_mounts_manifest", ) +def _generated_conf_impl(ctx): + output = ctx.actions.declare_file(ctx.attr.output_path) + ctx.actions.expand_template( + template = ctx.file.template, + output = output, + substitutions = { + "{PROJECT}": repr(ctx.attr.project), + "{PROJECT_URL}": repr(ctx.attr.project_url), + }, + ) + return [DefaultInfo(files = depset([output]))] + +_generated_conf = rule( + implementation = _generated_conf_impl, + attrs = { + "project": attr.string(mandatory = True), + "project_url": attr.string(mandatory = True), + "output_path": attr.string(mandatory = True), + "template": attr.label( + allow_single_file = True, + default = Label("@score_docs_as_code//:default_conf.py.tpl"), + ), + }, +) + def docs_bundle(name, source_dir = None, entry_doc = "index", bundles = [], scan_code = [], visibility = None, **kwargs): """A docs bundle, optionally composed of others. @@ -139,6 +164,8 @@ def _missing_requirements(deps): def docs( source_dir = "docs", + project = None, + project_url = None, data = [], deps = [], external_needs = [], @@ -154,6 +181,8 @@ def docs( Args: 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. deps: Additional dependencies for the documentation build. external_needs: List of external needs targets to include in the documentation build. @@ -175,7 +204,36 @@ def docs( """ # HINT: keep documentation sync docs/reference/bazel_macros.rst - source_config = ":" + ("" if source_dir == "." else source_dir + "/") + "conf.py" + config_file_path = join_path(source_dir, "conf.py") + sphinx_config_for_bazel_build = ":" + config_file_path + has_source_config = len(native.glob([config_file_path], allow_empty = True)) == 1 + + # This 0/1 list is appended to the run targets' data. + sphinx_config_for_bazel_run = [] + if not has_source_config: + if not project or not project_url: + fail("docs(): no " + config_file_path + " found; provide both project and project_url to docs().") + # Generate the config at the source-root location expected by + # sphinx_docs: that rule treats the config file's directory as the + # Sphinx source directory. + _generated_conf( + name = "_docs_generated_build_config", + project = project, + project_url = project_url, + output_path = config_file_path, + ) + sphinx_config_for_bazel_build = ":_docs_generated_build_config" + + # Generate the config at an internal location for ``bazel run`` + # targets. ``source_dir/conf.py`` would conflict with the generated + # ``:docs`` executable when source_dir is named ``docs``. + _generated_conf( + name = "_docs_generated_run_config", + project = project, + project_url = project_url, + output_path = "_docs_generated_config/conf.py", + ) + sphinx_config_for_bazel_run = [":_docs_generated_run_config"] # Convention in this macro: an optional Bazel label is named ``*_label`` # but represented as a 0/1 list. This lets it be appended directly to @@ -239,7 +297,7 @@ def docs( # complete bundle here would add those files to runfiles and could collide # with the executable target name (for example ``docs`` and ``docs/``). # External bundles do need runfiles, so keep only those sources. - docs_data = data + external_needs + metamodel_label + [":sourcelinks_json", ":_external_docs_runfiles"] + mounts_manifest_label + docs_data = data + external_needs + metamodel_label + [":sourcelinks_json", ":_external_docs_runfiles"] + mounts_manifest_label + sphinx_config_for_bazel_run docs_env = { "SOURCE_DIRECTORY": source_dir, @@ -252,6 +310,10 @@ def docs( "MOUNTS_MANIFEST": "$(rlocationpath :_mounts_manifest)" if bundles else "", "SCORE_SOURCELINKS": "$(location :sourcelinks_json)", } + if sphinx_config_for_bazel_run: + # The generated file is named conf.py in its own directory. The run + # targets pass that directory to Sphinx via -c. + docs_env["SPHINX_CONFIG_FILE"] = "$(rlocationpath :_docs_generated_run_config)" if metamodel: # The interactive ``py_binary`` targets run from a runfiles tree. # incremental.py resolves this logical path through ``RUNFILES_DIR``. @@ -314,7 +376,7 @@ def docs( sphinx_docs( name = "needs_json", srcs = [":docs_bundle"], - config = source_config, + config = sphinx_config_for_bazel_build, extra_opts = [ "-W", "--keep-going", diff --git a/docs/how-to/setup.md b/docs/how-to/setup.md index 2f7bca8c3..2306f097c 100644 --- a/docs/how-to/setup.md +++ b/docs/how-to/setup.md @@ -67,6 +67,8 @@ load("@score_docs_as_code//:docs.bzl", "docs") docs( source_dir = "", + project = "", + project_url = "https://example.com/", data = [ "@other_repo:needs_json", # Optional, if you have dependencies ], @@ -75,10 +77,19 @@ docs( For configuration options see {ref}`docs_bazel-macros`. -### 4. Copy conf.py +### 4. Optional: add conf.py -Copy the `conf.py` file from the `docs-as-code` module to your `source_dir`. +No `conf.py` is required for the default setup. The `docs()` macro generates +one from `project` and `project_url`; the Docs-as-Code version and baseline +extensions are supplied automatically. +Add a `conf.py` to your source directory only when you need additional Sphinx +configuration. When it exists, it remains the authoritative configuration. + +Note that conf.py will affect only local builds. It will not affect the integrated +documentation build by reference_integration. Local builds are useful for testing and +debugging your documentation before committing changes. Not for final delivery. HAving a +custom conf.py is highly discouraged. #### 5. Run a documentation build: diff --git a/docs/reference/bazel_macros.rst b/docs/reference/bazel_macros.rst index a14be1656..6d9aa7500 100644 --- a/docs/reference/bazel_macros.rst +++ b/docs/reference/bazel_macros.rst @@ -34,6 +34,8 @@ Minimal example (root ``BUILD``) docs( source_dir = "docs", + project = "My Project", + project_url = "https://example.com/my-project", data = [ # labels to any extra tools or data you want included # e.g. "//:needs_json" or other tool targets @@ -49,7 +51,13 @@ Minimal example (root ``BUILD``) - ``source_dir`` (string, default: ``"docs"``) Path (relative to repository root) to your Sphinx source directory. This is the folder - that contains your ``conf.py`` and the top-level ReST/markdown sources. + that contains the top-level ReST/markdown sources. A ``conf.py`` is optional. + +- ``project`` and ``project_url`` (strings, optional) + Project name and canonical project URL. They are required when ``source_dir`` + has no ``conf.py``; in that case ``docs()`` generates the Sphinx configuration + and supplies the Docs-as-Code baseline version and extensions. If a ``conf.py`` + exists, it remains authoritative and these values are not used. - ``data`` (list of bazel labels) Extra runfiles / data targets that should be made available to the documentation targets. diff --git a/src/incremental.py b/src/incremental.py index d1c6bc854..fdb692c4c 100644 --- a/src/incremental.py +++ b/src/incremental.py @@ -165,6 +165,15 @@ def _mounted_watch_dirs( f"--define=mounts_manifest={os.environ.get('MOUNTS_MANIFEST', '')}", ] + generated_config = os.environ.get("SPHINX_CONFIG_FILE", "") + if generated_config: + # Under ``bazel run`` this is a runfiles-relative path. Sphinx wants + # the directory containing a file literally named ``conf.py``. + config_file = Path(generated_config) + if not config_file.is_absolute(): + config_file = get_runfiles_dir() / config_file + base_arguments.extend(["-c", str(config_file.parent)]) + metamodel_yaml = os.environ.get("SCORE_METAMODEL_YAML", "") if metamodel_yaml: # ``docs`` passes a runfiles-relative path under ``bazel run``. Keep diff --git a/src/tests/docs_bzl/scenarios/basic_docs/BUILD b/src/tests/docs_bzl/scenarios/basic_docs/BUILD index 5353407ad..7ef270b83 100644 --- a/src/tests/docs_bzl/scenarios/basic_docs/BUILD +++ b/src/tests/docs_bzl/scenarios/basic_docs/BUILD @@ -15,5 +15,7 @@ load("//:docs.bzl", "docs") docs( source_dir = "docs", + project = "Basic Test", + project_url = "https://github.com/eclipse-score/docs-as-code", test_sources = ["src/tests/docs_bzl/scenarios/basic_docs"], ) diff --git a/src/tests/docs_bzl/scenarios/missing_docs_config/BUILD b/src/tests/docs_bzl/scenarios/missing_docs_config/BUILD new file mode 100644 index 000000000..f1105bcda --- /dev/null +++ b/src/tests/docs_bzl/scenarios/missing_docs_config/BUILD @@ -0,0 +1,12 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("//:docs.bzl", "docs") + +docs(source_dir = "docs") diff --git a/src/tests/docs_bzl/scenarios/missing_docs_config/docs/index.rst b/src/tests/docs_bzl/scenarios/missing_docs_config/docs/index.rst new file mode 100644 index 000000000..413df450a --- /dev/null +++ b/src/tests/docs_bzl/scenarios/missing_docs_config/docs/index.rst @@ -0,0 +1,16 @@ +.. + # ******************************************************************************* + # 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 + # ******************************************************************************* + +Missing configuration fixture +============================= diff --git a/src/tests/docs_bzl/test_basic_docs.py b/src/tests/docs_bzl/test_basic_docs.py index c6f8f3e06..9c3cb8347 100644 --- a/src/tests/docs_bzl/test_basic_docs.py +++ b/src/tests/docs_bzl/test_basic_docs.py @@ -32,3 +32,7 @@ def test_basic_docs_builds_html(): index_html = result.build_dir / "index.html" assert "Basic Test" in index_html.read_text(encoding="utf-8") + + +def test_basic_docs_builds_needs_without_conf_py(): + run_scenario("build", "basic_docs", ":needs_json") diff --git a/src/tests/docs_bzl/test_missing_docs_config.py b/src/tests/docs_bzl/test_missing_docs_config.py new file mode 100644 index 000000000..86a938078 --- /dev/null +++ b/src/tests/docs_bzl/test_missing_docs_config.py @@ -0,0 +1,18 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Validation of docs() configuration fallback.""" + +from src.tests.docs_bzl.helpers import run_scenario + + +def test_missing_conf_and_macro_values_fails_analysis(): + result = run_scenario("build", "missing_docs_config", ":docs", expect_error=True) + + assert "no docs/conf.py found" in result.stderr + assert "provide both project and project_url" in result.stderr From 523676c357df90ade4e5e024dabe0ad01fbbdef9 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Tue, 4 Aug 2026 11:38:51 +0200 Subject: [PATCH 2/3] leading by example --- BUILD | 2 ++ docs/conf.py | 22 ---------------------- 2 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 docs/conf.py diff --git a/BUILD b/BUILD index 030eb112d..4bb767ea8 100644 --- a/BUILD +++ b/BUILD @@ -20,6 +20,8 @@ exports_files([ ]) docs( + project = "S-CORE Docs-as-Code", + project_url = "https://eclipse-score.github.io/docs-as-code", external_needs = [ "@score_process//:needs_json_file", ], diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 56031d451..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,22 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 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 -# ******************************************************************************* -import matplotlib - -project = "Score Docs-as-Code" -project_url = "https://eclipse-score.github.io/docs-as-code/" -version = "0.1" - -extensions = [ - "score_sphinx_bundle", -] -matplotlib.rcParamsDefault["savefig.bbox"] = "tight" From 936b7e95ec1b9c4f5f38d99cfd62246294308e1d Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Tue, 4 Aug 2026 16:40:02 +0200 Subject: [PATCH 3/3] fix tests --- docs.bzl | 12 ++++++++++-- .../{BUILD => BUILD.negative} | 0 src/tests/docs_bzl/test_missing_docs_config.py | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) rename src/tests/docs_bzl/scenarios/missing_docs_config/{BUILD => BUILD.negative} (100%) diff --git a/docs.bzl b/docs.bzl index f7d9cadd3..7b087af3e 100644 --- a/docs.bzl +++ b/docs.bzl @@ -326,14 +326,22 @@ def docs( docs_env["ACTION"] = "incremental" py_binary( - name = "docs", - tags = ["cli_help=Build documentation:\nbazel run //:docs"], + # Generated documentation artifacts may live below ``docs/``. A + # py_binary named ``docs`` would own the conflicting Bazel output path + # ``docs``; expose this binary via the alias below instead. + name = "_score_docs_cli", srcs = [incremental_src], data = docs_data, deps = deps, env = docs_env ) + native.alias( + name = "docs", + actual = ":_score_docs_cli", + tags = ["cli_help=Build documentation:\nbazel run //:docs"], + ) + docs_env["ACTION"] = "linkcheck" py_binary( name = "docs_link_check", diff --git a/src/tests/docs_bzl/scenarios/missing_docs_config/BUILD b/src/tests/docs_bzl/scenarios/missing_docs_config/BUILD.negative similarity index 100% rename from src/tests/docs_bzl/scenarios/missing_docs_config/BUILD rename to src/tests/docs_bzl/scenarios/missing_docs_config/BUILD.negative diff --git a/src/tests/docs_bzl/test_missing_docs_config.py b/src/tests/docs_bzl/test_missing_docs_config.py index 86a938078..c8511a08a 100644 --- a/src/tests/docs_bzl/test_missing_docs_config.py +++ b/src/tests/docs_bzl/test_missing_docs_config.py @@ -8,11 +8,24 @@ # ******************************************************************************* """Validation of docs() configuration fallback.""" -from src.tests.docs_bzl.helpers import run_scenario +from src.tests.docs_bzl.helpers import repo_root, run_scenario def test_missing_conf_and_macro_values_fails_analysis(): - result = run_scenario("build", "missing_docs_config", ":docs", expect_error=True) + scenario_dir = repo_root() / "src/tests/docs_bzl/scenarios/missing_docs_config" + build_file = scenario_dir / "BUILD" + fixture = scenario_dir / "BUILD.negative" + assert not build_file.exists(), ( + "negative test package must not be discovered by //..." + ) + + build_file.write_text(fixture.read_text(encoding="utf-8"), encoding="utf-8") + try: + result = run_scenario( + "build", "missing_docs_config", ":docs", expect_error=True + ) + finally: + build_file.unlink() assert "no docs/conf.py found" in result.stderr assert "provide both project and project_url" in result.stderr