Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
load("//:docs.bzl", "docs")

package(default_visibility = ["//visibility:public"])
exports_files(["pyproject.toml"])
exports_files([
"default_conf.py.tpl",
"pyproject.toml",
])

docs(
project = "S-CORE Docs-as-Code",
project_url = "https://eclipse-score.github.io/docs-as-code",
external_needs = [
"@score_process//:needs_json_file",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
80 changes: 75 additions & 5 deletions docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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 = [], code_targets = [], visibility = None, **kwargs):
"""A docs bundle, optionally composed of others.

Expand Down Expand Up @@ -150,6 +175,8 @@ def _missing_requirements(deps):

def docs(
source_dir = "docs",
project = None,
project_url = None,
data = [],
deps = [],
external_needs = [],
Expand All @@ -166,6 +193,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.
Expand All @@ -191,7 +220,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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use the bundle sources instead of globbing yourself again here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.py is not included in the bundle sources


# 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(
Comment on lines +243 to +246
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
Expand Down Expand Up @@ -256,7 +314,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,
Expand All @@ -269,6 +327,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``.
Expand All @@ -281,14 +343,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",
Expand Down Expand Up @@ -331,7 +401,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",
Expand Down
22 changes: 0 additions & 22 deletions docs/conf.py

This file was deleted.

15 changes: 13 additions & 2 deletions docs/how-to/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ load("@score_docs_as_code//:docs.bzl", "docs")

docs(
source_dir = "<your sphinx source dir>",
project = "<your project name>",
project_url = "https://example.com/<your-project>",
data = [
"@other_repo:needs_json", # Optional, if you have dependencies
],
Expand All @@ -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.
Comment on lines +89 to +92

#### 5. Run a documentation build:

Expand Down
10 changes: 9 additions & 1 deletion docs/reference/bazel_macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions src/incremental.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/tests/docs_bzl/scenarios/basic_docs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
12 changes: 12 additions & 0 deletions src/tests/docs_bzl/scenarios/missing_docs_config/BUILD.negative
Original file line number Diff line number Diff line change
@@ -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")
16 changes: 16 additions & 0 deletions src/tests/docs_bzl/scenarios/missing_docs_config/docs/index.rst
Original file line number Diff line number Diff line change
@@ -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
=============================
4 changes: 4 additions & 0 deletions src/tests/docs_bzl/test_basic_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
31 changes: 31 additions & 0 deletions src/tests/docs_bzl/test_missing_docs_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# *******************************************************************************
# 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 repo_root, run_scenario


def test_missing_conf_and_macro_values_fails_analysis():
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
Loading