Skip to content
Open
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
134 changes: 89 additions & 45 deletions docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,36 @@ load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")

def _rewrite_needs_json_to_docs_sources(labels):
"""Replace '@repo//:needs_json' -> '@repo//:docs_sources' for every item."""
"""Replace '@repo//:needs_json' -> '@repo//:docs_sources' for every item.

Also handles a producer using a custom docs() `name`
(e.g. '@repo//:foo_needs_json' -> '@repo//:foo_docs_sources').
"""
out = []
for x in labels:
s = str(x)
if s.endswith("//:needs_json"):
out.append(s.replace("//:needs_json", "//:docs_sources"))
elif "//:" in s and s.endswith("_needs_json"):
out.append(s[:-len("needs_json")] + "docs_sources")
else:
out.append(s)
return out

def _rewrite_needs_json_to_sourcelinks(labels):
"""Replace '@repo//:needs_json' -> '@repo//:sourcelinks_json' for every item."""
"""Replace '@repo//:needs_json' -> '@repo//:sourcelinks_json' for every item.

Also handles a producer using a custom docs() `name`
(e.g. '@repo//:foo_needs_json' -> '@repo//:foo_sourcelinks_json').
"""
out = []
for x in labels:
s = str(x)
if s.endswith("//:needs_json"):
out.append(s.replace("//:needs_json", "//:sourcelinks_json"))
#Items which do not end up with '//:needs_json' shall not be appended to 'out'.
elif "//:" in s and s.endswith("_needs_json"):
out.append(s[:-len("needs_json")] + "sourcelinks_json")
#Items which do not end up with a 'needs_json' target shall not be appended to 'out'.
#They are treated separately and are not related to source code linking.
return out

Expand Down Expand Up @@ -127,12 +139,21 @@ def _missing_requirements(deps):
fail(msg)
fail("This case should be unreachable?!")

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

By using this function, you'll get any and all updates for documentation targets in one place.

The macro can be called multiple times in the same BUILD file as long as each call uses a

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.

I'm not convinced that this is a good idea.

There can only be exactly one documentation hosted by github.io. What is the motivation to have more?

The need to distribute documentation across the repo is addressed by implemented DR-008-infra.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes there can be only one hosted, that is correct.

This is to allow for multiple docs to be build, without it being hosted.
Basically many independent documentations in one repo and allow / enable them all to be build without them needing to be hosted.
This is also in anticipation of the split up we might need to do in regards to trlc things.

distinct `name`. When `name` is left at its default ("docs"), all generated targets keep their
historical, unprefixed names (e.g. `needs_json`, `sourcelinks_json`, `docs_check`) for backward
compatibility. For any other `name`, every generated target is prefixed with it
(e.g. `name = "foo"` yields `foo`, `foo_check`, `foo_needs_json`, `foo_sourcelinks_json`, ...).

Args:
name: Name of the main documentation target. Defaults to "docs". All other targets created
by this macro (needs_json, sourcelinks_json, docs_check, ide_support, etc.) are derived
from this name, so multiple calls to docs() in the same BUILD file must use distinct names.
source_dir: The source directory containing documentation files. Defaults to "docs".
data: Additional data files to include in the documentation build.
deps: Additional dependencies for the documentation build.
Expand All @@ -142,10 +163,31 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
file instead of the default metamodel shipped with score_metamodel.
"""

call_path = native.package_name()

if call_path != "":
fail("docs() must be called from the root package. Current package: " + call_path)
def _n(default_name):
"""Derive a target name: keep historical unprefixed names when name == "docs", otherwise prefix with `name`."""
if name == "docs":
return default_name
if default_name == "docs":
return name
if default_name.startswith("docs_"):
return name + "_" + default_name[len("docs_"):]
return name + "_" + default_name

sphinx_build_name = _n("sphinx_build")
docs_sources_name = _n("docs_sources")
sourcelinks_json_name = _n("sourcelinks_json")
merged_sourcelinks_name = _n("merged_sourcelinks")
docs_name = _n("docs")
docs_combo_name = _n("docs_combo")
docs_combo_experimental_name = _n("docs_combo_experimental")
docs_link_check_name = _n("docs_link_check")
docs_check_name = _n("docs_check")
live_preview_name = _n("live_preview")
live_preview_combo_name = _n("live_preview_combo_experimental")
ide_support_name = _n("ide_support")
needs_json_name = _n("needs_json")
metrics_json_name = _n("metrics_json")
traceability_gate_name = _n("traceability_gate")

metamodel_data = []
metamodel_env = {}
Expand All @@ -165,7 +207,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
incremental_src = Label("//src:incremental.py")

sphinx_build_binary(
name = "sphinx_build",
name = sphinx_build_name,
visibility = ["//visibility:private"],
data = data + metamodel_data,
deps = deps,
Expand All @@ -179,7 +221,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
source_prefix = source_dir + "/"

native.filegroup(
name = "docs_sources",
name = docs_sources_name,
srcs = native.glob([
source_prefix + "**/*.png",
source_prefix + "**/*.svg",
Expand All @@ -197,23 +239,23 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
visibility = ["//visibility:public"],
)

_sourcelinks_json(name = "sourcelinks_json", srcs = scan_code)
_sourcelinks_json(name = sourcelinks_json_name, srcs = scan_code)

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 + metamodel_data + [":sourcelinks_json"]
combo_data = data_with_docs_sources + metamodel_data + [":merged_sourcelinks"]
_merge_sourcelinks(name = merged_sourcelinks_name, sourcelinks = [":" + sourcelinks_json_name] + additional_combo_sourcelinks, known_good = known_good)
docs_data = data + metamodel_data + [":" + sourcelinks_json_name]
combo_data = data_with_docs_sources + metamodel_data + [":" + merged_sourcelinks_name]

docs_env = {
"SOURCE_DIRECTORY": source_dir,
"DATA": str(data),
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
"SCORE_SOURCELINKS": "$(location :%s)" % sourcelinks_json_name,
} | metamodel_env
docs_sources_env = {
"SOURCE_DIRECTORY": source_dir,
"DATA": str(data_with_docs_sources),
"SCORE_SOURCELINKS": "$(location :merged_sourcelinks)",
"SCORE_SOURCELINKS": "$(location :%s)" % merged_sourcelinks_name,
} | metamodel_env
if known_good:
known_good_str = str(known_good)
Expand All @@ -225,8 +267,8 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
docs_env["ACTION"] = "incremental"

py_binary(
name = "docs",
tags = ["cli_help=Build documentation:\nbazel run //:docs"],
name = docs_name,
tags = ["cli_help=Build documentation:\nbazel run //:%s" % docs_name],
srcs = [incremental_src],
data = docs_data,
deps = deps,
Expand All @@ -235,24 +277,24 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =

docs_sources_env["ACTION"] = "incremental"
py_binary(
name = "docs_combo",
tags = ["cli_help=Build full documentation with all dependencies:\nbazel run //:docs_combo"],
name = docs_combo_name,
tags = ["cli_help=Build full documentation with all dependencies:\nbazel run //:%s" % docs_combo_name],
srcs = [incremental_src],
data = combo_data,
deps = deps,
env = docs_sources_env
)

native.alias(
name = "docs_combo_experimental",
actual = ":docs_combo",
deprecation = "Target '//:docs_combo_experimental' is deprecated. Use '//:docs_combo' instead.",
name = docs_combo_experimental_name,
actual = ":" + docs_combo_name,
deprecation = "Target '//:%s' is deprecated. Use '//:%s' instead." % (docs_combo_experimental_name, docs_combo_name),
)

docs_env["ACTION"] = "linkcheck"
py_binary(
name = "docs_link_check",
tags = ["cli_help=Verify Links inside Documentation:\nbazel run //:link_check\n (Note: this could take a long time)"],
name = docs_link_check_name,
tags = ["cli_help=Verify Links inside Documentation:\nbazel run //:%s\n (Note: this could take a long time)" % docs_link_check_name],
srcs = [incremental_src],
data = docs_data,
deps = deps,
Expand All @@ -261,8 +303,8 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =

docs_env["ACTION"] = "check"
py_binary(
name = "docs_check",
tags = ["cli_help=Verify documentation:\nbazel run //:docs_check"],
name = docs_check_name,
tags = ["cli_help=Verify documentation:\nbazel run //:%s" % docs_check_name],
srcs = [incremental_src],
data = docs_data,
deps = deps,
Expand All @@ -271,8 +313,8 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =

docs_env["ACTION"] = "live_preview"
py_binary(
name = "live_preview",
tags = ["cli_help=Live preview documentation in the browser:\nbazel run //:live_preview"],
name = live_preview_name,
tags = ["cli_help=Live preview documentation in the browser:\nbazel run //:%s" % live_preview_name],
srcs = [incremental_src],
data = docs_data,
deps = deps,
Expand All @@ -281,26 +323,27 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =

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"],
name = live_preview_combo_name,
tags = ["cli_help=Live preview full documentation with all dependencies in the browser:\nbazel run //:%s" % live_preview_combo_name],
srcs = [incremental_src],
data = combo_data,
deps = deps,
env = docs_sources_env
)

venv_name = ".venv_docs" if name == "docs" else ".venv_" + name
py_venv(
name = "ide_support",
tags = ["cli_help=Create virtual environment (.venv_docs) for documentation support:\nbazel run //:ide_support"],
venv_name = ".venv_docs",
name = ide_support_name,
tags = ["cli_help=Create virtual environment (%s) for documentation support:\nbazel run //:%s" % (venv_name, ide_support_name)],
venv_name = venv_name,
deps = deps,
data = data,
package_collisions = "warning",
)

sphinx_docs(
name = "needs_json",
srcs = [":docs_sources"],
name = needs_json_name,
srcs = [":" + docs_sources_name],
config = ":" + source_prefix + "conf.py",
extra_opts = [
"-W",
Expand All @@ -309,30 +352,31 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
"--jobs",
"auto",
"--define=external_needs_source=" + str(data),
"--define=score_sourcelinks_json=$(location :sourcelinks_json)",
"--define=score_sourcelinks_json=$(location :%s)" % sourcelinks_json_name,
"--define=score_source_code_linker_plain_links=1",
],
formats = ["needs"],
sphinx = ":sphinx_build",
tools = data + [":sourcelinks_json"],
sphinx = ":" + sphinx_build_name,
tools = data + [":" + sourcelinks_json_name],
visibility = ["//visibility:public"],
# Persistent workers cause stale symlinks after dependency version
# changes, corrupting the Bazel cache.
allow_persistent_workers = False,
)

metrics_json_out = "metrics.json" if name == "docs" else name + "_metrics.json"
native.genrule(
name = "metrics_json",
srcs = [":needs_json"],
outs = ["metrics.json"],
cmd = "cp $(location :needs_json)/metrics.json $@",
name = metrics_json_name,
srcs = [":" + needs_json_name],
outs = [metrics_json_out],
cmd = "cp $(location :%s)/metrics.json $@" % needs_json_name,
visibility = ["//visibility:public"],
)

native.alias(
name = "traceability_gate",
name = traceability_gate_name,
actual = Label("//scripts_bazel:traceability_gate"),
tags = ["cli_help=Enforce traceability coverage thresholds:\nbazel run //:traceability_gate -- --metrics-json $(location //:metrics_json)"],
tags = ["cli_help=Enforce traceability coverage thresholds:\nbazel run //:%s -- --metrics-json $(location //:%s)" % (traceability_gate_name, metrics_json_name)],
)

def _sourcelinks_json(name, srcs):
Expand Down
4 changes: 4 additions & 0 deletions docs/how-to/other_modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Example `BUILD` snippet (consumer module):
source_dir = "docs",
)

If the other module's ``docs()`` call uses a custom ``name`` (see :doc:`bazel_macros
</reference/bazel_macros>`), reference its prefixed target instead, e.g.
``"@score_process//:foo_needs_json"``.

More details in :ref:`docs_bidirectional_traceability`.

3) Reference needs across modules
Expand Down
4 changes: 4 additions & 0 deletions docs/how-to/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@ The `docs()` macro accepts the following arguments:

| Parameter | Description | Required |
|-----------|-------------|----------|
| `name` | Name of the main documentation target. Defaults to `"docs"`. Only needed when calling `docs()` more than once in the same `BUILD` file; each call must then use a distinct name, and all generated targets are prefixed with it (see {doc}`bazel_macros </reference/bazel_macros>`) | No |
| `source_dir` | Directory of documentation source files (RST, MD) | Yes |
| `data` | List of `needs_json` targets that should be included in the documentation | No |
| `deps` | Additional Bazel Python dependencies | No |
| `scan_code` | Source code targets to scan for traceability tags | No |
| `known_good` | Label to a "known good" JSON file for source links | No |
| `metamodel` | Label to a custom `metamodel.yaml` that replaces the default metamodel | No |

If you set a custom `name`, and also use non-Bazel execution (e.g. Esbonio/IDE support),
add `docs_target_name = "<your name>"` to your `conf.py` so it matches.

### 4. Copy conf.py

Copy the `conf.py` file from the `docs-as-code` module to your `source_dir`.
Expand Down
26 changes: 26 additions & 0 deletions docs/reference/bazel_macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ Minimal example (root ``BUILD``)
],
)

- ``name`` (string, default: ``"docs"``)
Name of the main documentation target. When left at its default, all generated targets
keep their historical, unprefixed names (``needs_json``, ``sourcelinks_json``,
``docs_check``, ``ide_support``, etc.) for backward compatibility. For any other name,
every generated target is prefixed with it, e.g. ``name = "foo"`` yields ``foo``,
``foo_check``, ``foo_needs_json``, ``foo_sourcelinks_json``, ``foo_ide_support``, and so on.

Use a custom ``name`` when you need to call ``docs()`` more than once in the same
``BUILD`` file (e.g. to build two independent documentation sets); each call must then
use a distinct ``name``.

.. code-block:: python

docs(
name = "foo",
source_dir = "docs_foo",
)

If you use a custom ``name`` and also rely on non-Bazel execution (e.g. Esbonio/IDE
support via ``ide_support``), set ``docs_target_name = "foo"`` in that ``source_dir``'s
``conf.py`` to match, so the tooling can resolve the correctly-prefixed runfiles and
Bazel targets outside of a ``bazel run`` invocation.

- ``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.
Expand Down Expand Up @@ -92,3 +115,6 @@ Edge cases
targets are included in the ``data`` list so they are available to the build driver.
- The experimental "combo" targets rewrite some ``data`` labels for combined builds; those
are intended for advanced use and are optional for normal doc workflows.
- If you depend on another module's ``docs()`` output via ``data``, and that module used a
custom ``name``, reference its prefixed target (e.g. ``@other_repo//:foo_needs_json``)
instead of the default ``needs_json``.
4 changes: 4 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
| ----------------------------- | ------------------------------------------- |
| `bazel build //:needs_json` | Creates a 'needs.json' file |
| `bazel build //:docs_sources` | Provides all the documentation source files |

If the `docs()` macro is called with a custom `name` (see {doc}`bazel_macros`), every
target above is prefixed with it instead, e.g. `name = "foo"` yields `//:foo`,
`//:foo_check`, `//:foo_needs_json`, `//:foo_ide_support`, etc.
2 changes: 2 additions & 0 deletions src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
app.add_config_value("external_needs_source", "", rebuild="env")
app.add_config_value("score_metamodel_yaml", "", rebuild="env")
app.add_config_value("required_in_id", [], rebuild="env")
# Registered here too (guarded) so score_metamodel also works when used standalone,
# without score_sphinx_bundle (e.g. this extension's own file-based test fixtures).
config_setdefault(app.config, "needs_id_required", True)
config_setdefault(app.config, "needs_id_regex", "^[A-Za-z0-9_-]{6,}")

Expand Down
Loading
Loading