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
3 changes: 3 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ docs(
data = [
"@score_process//:needs_json",
],
extra_docs = [
"//src:src_docs",
],
scan_code = [
"//scripts_bazel:sources",
"//src:all_sources",
Expand Down
98 changes: 77 additions & 21 deletions docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,59 @@ Easy streamlined way for S-CORE docs-as-code.
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_venv")
load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
load("@rules_python//sphinxdocs/private:sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo")

def _docs_source_tree_impl(ctx):
"""Materializes a sphinx_docs_library into a single directory for incremental builds."""
output_dir = ctx.actions.declare_directory(ctx.label.name)

all_inputs = []
pairs = [] # list of (src_exec_path, dest_rel_path)

# conf.py at its natural short_path position (e.g. "docs/conf.py")
config = ctx.file.config
all_inputs.append(config)
pairs.append((config.path, config.short_path))

for t in ctx.attr.lib:
info = t[SphinxDocsLibraryInfo]
for entry in info.transitive.to_list():
for f in entry.files:
dest_rel = entry.prefix + f.short_path.removeprefix(entry.strip_prefix)
all_inputs.append(f)
pairs.append((f.path, dest_rel))

cmds = ["set -euo pipefail"]
for src, dest_rel in pairs:
parent = dest_rel.rsplit("/", 1)[0] if "/" in dest_rel else ""
if parent:
cmds.append("mkdir -p '{}/{}'".format(output_dir.path, parent))
cmds.append("ln '{}' '{}/{}'".format(src, output_dir.path, dest_rel))

ctx.actions.run_shell(
inputs = all_inputs,
outputs = [output_dir],
command = "\n".join(cmds),
progress_message = "Materializing docs source tree for %{label}",
)

return [DefaultInfo(files = depset([output_dir]))]

_docs_source_tree = rule(
implementation = _docs_source_tree_impl,
attrs = {
"lib": attr.label_list(
providers = [SphinxDocsLibraryInfo],
doc = "sphinx_docs_library targets whose files to merge into the source tree.",
),
"config": attr.label(
allow_single_file = True,
mandatory = True,
doc = "The conf.py file to include in the source tree.",
),
},
)

def _rewrite_needs_json_to_docs_sources(labels):
"""Replace '@repo//:needs_json' -> '@repo//:docs_sources' for every item."""
Expand Down Expand Up @@ -125,7 +178,9 @@ def _missing_requirements(deps):
fail(msg)
fail("This case should be unreachable?!")

def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = None):


def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = None, extra_docs = []):
"""Creates all targets related to documentation.

By using this function, you'll get any and all updates for documentation targets in one place.
Expand All @@ -135,6 +190,9 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
data: Additional data files to include in the documentation build.
deps: Additional dependencies for the documentation build.
scan_code: List of code targets to scan for source code links.
extra_docs: List of sphinx_docs_library targets to merge into the source tree.
Each target controls placement via its strip_prefix/prefix attributes.
See sphinx_docs_library in rules_python for details.
"""

call_path = native.package_name()
Expand Down Expand Up @@ -163,7 +221,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
else:
source_prefix = source_dir + "/"

native.filegroup(
sphinx_docs_library(
name = "docs_sources",
srcs = native.glob([
source_prefix + "**/*.png",
Expand All @@ -179,6 +237,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
source_prefix + "**/*.csv",
source_prefix + "**/*.inc",
], allow_empty = True),
deps = extra_docs,
visibility = ["//visibility:public"],
)

Expand All @@ -187,11 +246,19 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
data_with_docs_sources = _rewrite_needs_json_to_docs_sources(data)
additional_combo_sourcelinks = _rewrite_needs_json_to_sourcelinks(data)
_merge_sourcelinks(name = "merged_sourcelinks", sourcelinks = [":sourcelinks_json"] + additional_combo_sourcelinks, known_good = known_good)
docs_data = data + [":sourcelinks_json"]
combo_data = data_with_docs_sources + [":merged_sourcelinks"]
_docs_source_tree(
name = "docs_src_dir",
lib = [":docs_sources"],
config = ":" + source_prefix + "conf.py",
visibility = ["//visibility:private"],
)

docs_data = data + [":sourcelinks_json", ":docs_sources", ":docs_src_dir"]
combo_data = data_with_docs_sources + [":merged_sourcelinks", ":docs_sources"]

docs_env = {
"SOURCE_DIRECTORY": source_dir,
"DOCS_SOURCE_TREE": "$(rlocationpath :docs_src_dir)",
"DATA": str(data),
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
}
Expand Down Expand Up @@ -253,24 +320,12 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
env = docs_env
)

docs_env["ACTION"] = "live_preview"
py_binary(
name = "live_preview",
tags = ["cli_help=Live preview documentation in the browser:\nbazel run //:live_preview"],
srcs = ["@score_docs_as_code//src:incremental.py"],
data = docs_data,
deps = deps,
env = docs_env
)

docs_sources_env["ACTION"] = "live_preview"
py_binary(
name = "live_preview_combo_experimental",
tags = ["cli_help=Live preview full documentation with all dependencies in the browser:\nbazel run //:live_preview_combo_experimental"],
srcs = ["@score_docs_as_code//src:incremental.py"],
data = combo_data,
name = "gen_live_preview",
tags = ["cli_help=Generate ./live_preview script (run that script for a live preview):\nbazel run //:gen_live_preview"],
srcs = ["@score_docs_as_code//src:gen_live_preview.py"],
deps = deps,
env = docs_sources_env
env = {"SOURCE_DIRECTORY": source_dir},
)

py_venv(
Expand All @@ -284,8 +339,9 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =

sphinx_docs(
name = "needs_json",
srcs = [":docs_sources"],
srcs = [],
config = ":" + source_prefix + "conf.py",
deps = [":docs_sources"],
extra_opts = [
"-W",
"--keep-going",
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ It provides documentation, requirements, and traceability.
reference/index
concepts/index
internals/index
BLA/index
10 changes: 10 additions & 0 deletions src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
load("@aspect_rules_py//py:defs.bzl", "py_library")
load("@rules_java//java:java_binary.bzl", "java_binary")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")

# These are only exported because they're passed as files to the //docs.bzl
# macros, and thus must be visible to other packages. They should only be
Expand All @@ -26,6 +27,7 @@ exports_files(
"incremental.py",
"dummy.py",
"generate_sourcelinks_cli.py",
"gen_live_preview.py",
],
visibility = ["//visibility:public"],
)
Expand Down Expand Up @@ -94,3 +96,11 @@ filegroup(
],
visibility = ["//visibility:public"],
)

sphinx_docs_library(
name = "src_docs",
srcs = glob(["docs/**/*.rst"]),
strip_prefix = "src/docs/",
prefix = "docs/BLA/", # must be under docs(source_dir=X) !
visibility = ["//visibility:public"],
)
19 changes: 19 additions & 0 deletions src/docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
..
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

Source Extensions
=================

.. toctree::

overview
requirements
18 changes: 18 additions & 0 deletions src/docs/overview.rst
Original file line number Diff line number Diff line change
@@ -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.

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

Overview
========

The ``src/`` directory contains the Python extensions and Bazel helpers that
make up the docs-as-code toolchain.
Each extension is a standalone Sphinx plugin loaded via ``score_sphinx_bundle``.
25 changes: 25 additions & 0 deletions src/docs/requirements.rst
Original file line number Diff line number Diff line change
@@ -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

Requirements
============

.. tool_req:: Supports extra_docs in docs() macro
:id: tool_req__docs_extra_docs
:implemented: YES
:tags: Architecture
:version: 1

The ``docs()`` Bazel macro shall accept an ``extra_docs`` parameter
containing a list of ``sphinx_docs_library`` targets.
Their files shall be merged into the Sphinx source tree at the paths
determined by each library's ``strip_prefix`` and ``prefix`` attributes.
8 changes: 7 additions & 1 deletion src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ def _run_checks(app: Sphinx, exception: Exception | None) -> None:

ws_root = os.environ.get("BUILD_WORKSPACE_DIRECTORY", None)
cwd_or_ws_root = Path(ws_root) if ws_root else Path.cwd()
prefix = str(Path(app.srcdir).relative_to(cwd_or_ws_root))
try:
prefix = str(Path(app.srcdir).relative_to(cwd_or_ws_root))
except ValueError:
# srcdir is outside the workspace (e.g. a Bazel runfiles tree).
# prefix is only used for log-message locations, which log.py already
# skips when RUNFILES_DIR is set, so any fallback is fine here.
prefix = str(app.srcdir)

log = CheckLogger(logger, prefix)

Expand Down
101 changes: 101 additions & 0 deletions src/gen_live_preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# *******************************************************************************
# 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
# *******************************************************************************

"""Generates a live_preview script in the workspace root.

Usage: bazel run //:gen_live_preview
The generated ./live_preview script uses ibazel to keep :docs_src_dir fresh
and sphinx-autobuild to serve with browser auto-reload.
"""

import os
import stat
import textwrap
from pathlib import Path

if __name__ == "__main__":
workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY", "")
if not workspace:
raise SystemExit("Must be run via 'bazel run', not directly.")

source_dir = os.environ["SOURCE_DIRECTORY"]

script = textwrap.dedent(f"""\
#!/usr/bin/env python3
# Generated by: bazel run //:gen_live_preview
# Re-run that command to regenerate after changing docs() parameters.

import os
import subprocess
import sys
import threading
from pathlib import Path

WORKSPACE = Path({repr(workspace)})
SOURCE_DIR = {repr(source_dir)}
SRC_TREE = WORKSPACE / "bazel-bin" / "docs_src_dir" / SOURCE_DIR
BUILD_DIR = WORKSPACE / "_build"
VENV = WORKSPACE / ".venv_docs"
PORT = int(os.environ.get("PORT", "8000"))


def _ibazel_loop() -> None:
\"\"\"Runs ibazel to keep :docs_src_dir up-to-date.\"\"\"
subprocess.run(
["ibazel", "build", ":docs_src_dir"],
cwd=WORKSPACE,
)


if __name__ == "__main__":
sphinx_autobuild = VENV / "bin" / "sphinx-autobuild"
python = VENV / "bin" / "python"
if not python.exists():
sys.exit(
"Missing .venv_docs — run: bazel run //:ide_support"
)

# Initial build of the source tree
print("Building source tree (initial)...", flush=True)
subprocess.run(
["bazel", "build", ":docs_src_dir"], cwd=WORKSPACE, check=True
)

# Start ibazel in background to keep source tree fresh
threading.Thread(target=_ibazel_loop, daemon=True).start()

# sphinx-autobuild watches the materialized tree and handles reload
print(f"Serving at http://localhost:{{PORT}}")
print("Edit docs sources — the page will reload automatically.")
print("Ctrl-C to stop.")
cmd = [str(sphinx_autobuild)] if sphinx_autobuild.exists() else [
str(python), "-m", "sphinx_autobuild",
]
subprocess.run(
cmd + [
str(SRC_TREE),
str(BUILD_DIR),
"--jobs", "auto",
"--define=skip_rescanning_via_source_code_linker=1",
f"--define=needscfg_outpath={{WORKSPACE / SOURCE_DIR / 'ubproject.toml'}}",
f"--port={{PORT}}",
],
cwd=WORKSPACE,
)
""")

output = Path(workspace) / "live_preview"
output.write_text(script)
output.chmod(output.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
print(f"Generated {output}")
print("Run it with: ./live_preview")
4 changes: 3 additions & 1 deletion src/helper_lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def config_setdefault(config: Config, name: str, value: Any) -> None:
# Sphinx has no public API for this check. We use ``_raw_config`` which is the
# de-facto standard across the ecosystem (Furo, RTD-theme, etc.). If Sphinx
# ever adds a public alternative, update this single function.
if name not in config._raw_config: # pyright: ignore [reportPrivateUsage]
#
# Also check config.overrides so -D flags to Sphinx can override extension defaults
if name not in config._raw_config and name not in config.overrides: # pyright: ignore [reportPrivateUsage]
setattr(config, name, value)


Expand Down
Loading
Loading