Skip to content

Commit 684d546

Browse files
committed
feat: integrate sphinx-mounts for external source bundles
Wires the sphinx-mounts extension into the docs() Bazel macro so RST or Markdown content that lives outside docs/ (generated under bazel-bin, or in-repo next to its source code) can be surfaced in the Sphinx build without copying or symlinking. Files stay where they live and are visible to Sphinx via absolute paths registered through TOML. Why the TOML angle matters: ubCode (and any non-Python tool) reads ubproject.toml to discover the project. With mount entries in the host TOML and a sanitized ubproject.toml at each in-repo bundle's source root, the IDE resolves both the host and the bundle without ever invoking Sphinx -- giving as-you-type validation that a Sphinx-mediated workflow cannot match. This is an alternative to the materialize-a-tree approach; both can coexist, but the mount surface is lighter-weight and IDE-friendly. Surface added ------------- * mount(label, mount_at, attach_to=None, entry_doc="index", src_root=None) helper in docs.bzl. Mount entries pass through env MOUNTS as JSON, symmetric to data = [...] / external_needs_source. * files_to_dir Starlark rule that materialises a glob of files into a single declared-directory output under bazel-bin (the shape that sphinx-mounts' dir mode walks). * score_mounts Sphinx extension that: - parses MOUNTS, resolves runfile paths, - sets config.mounts in-memory for sphinx-mounts at build time, - emits a [[mounts]] fragment with portable confdir-relative paths pointing at the *real source location* (not the bazel-bin copy), so IDE jump-to-definition lands on the file the author wrote, - generates a sanitized per-bundle ubproject.toml at each in-repo bundle's src_root for ubCode's directory-walk project lookup. * New //:docs_html sandboxed HTML build target alongside //:needs_json. * Comprehensive how-to at docs/how-to/mount_external_sources.rst. Demo ---- * src/docs/ ships a small "Code Docs" bundle (index, overview, requirements). Mounted under docs/internals/code_docs/ with attach_to = "internals/index" so the host toctree auto-extends. * tool_req__docs_mounts (in the bundle) is referenced from the host via :implements:, exercising cross-bundle traceability as a first-class sphinx-needs link relation. Required ancillary changes -------------------------- * score_sync_toml: flip needscfg_exclude_defaults to False. needs.types, needs.links, needs.fields were being stripped from the generated ubproject.toml because they happen to compare equal to sphinx-needs' declared defaults; the result was a TOML with no type catalogue, so IDE tooling could not resolve project directives like tool_req. * score_metamodel/metamodel.yaml: allow tool_req -> tool_req via :implements: (existing optional_links only permitted satisfies to gd_req/stkh_req/feat_req/comp_req). * sphinx_mounts and tomli-w added to src/requirements.{in,txt}.
1 parent c6e9f81 commit 684d546

25 files changed

Lines changed: 1420 additions & 30 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ user.bazelrc
1111
# docs build artifacts
1212
/_build*
1313
docs/ubproject.toml
14+
src/docs/ubproject.toml
1415

1516
# Vale - editorial style guide
1617
.vale.ini

BUILD

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# SPDX-License-Identifier: Apache-2.0
1212
# *******************************************************************************
1313

14-
load("//:docs.bzl", "docs")
14+
load("//:docs.bzl", "docs", "mount")
1515

1616
package(default_visibility = ["//visibility:public"])
1717
exports_files(["pyproject.toml"])
@@ -20,6 +20,14 @@ docs(
2020
data = [
2121
"@score_process//:needs_json",
2222
],
23+
mounts = [
24+
mount(
25+
label = "//src:docs_dir",
26+
mount_at = "internals/code_docs",
27+
attach_to = "internals/index",
28+
src_root = "src/docs",
29+
),
30+
],
2331
scan_code = [
2432
"//scripts_bazel:sources",
2533
"//src:all_sources",

docs.bzl

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,38 @@ load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_venv")
4545
load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")
4646
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
4747

48+
def _files_to_dir_impl(ctx):
49+
out = ctx.actions.declare_directory(ctx.label.name)
50+
prefix = ctx.attr.strip_prefix
51+
cmds = ["set -euo pipefail"]
52+
for f in ctx.files.srcs:
53+
rel = f.short_path
54+
if prefix and rel.startswith(prefix):
55+
rel = rel[len(prefix):]
56+
rel = rel.lstrip("/")
57+
parent = "/".join(rel.split("/")[:-1])
58+
if parent:
59+
cmds.append("mkdir -p '{}/{}'".format(out.path, parent))
60+
cmds.append("cp '{}' '{}/{}'".format(f.path, out.path, rel))
61+
ctx.actions.run_shell(
62+
inputs = ctx.files.srcs,
63+
outputs = [out],
64+
command = "\n".join(cmds),
65+
progress_message = "Materializing files into directory %{label}",
66+
)
67+
return [DefaultInfo(files = depset([out]))]
68+
69+
files_to_dir = rule(
70+
implementation = _files_to_dir_impl,
71+
attrs = {
72+
"srcs": attr.label_list(allow_files = True, mandatory = True),
73+
"strip_prefix": attr.string(default = ""),
74+
},
75+
doc = "Materialize a list of source files into a single output " +
76+
"directory under bazel-bin. Used by docs() to produce mountable " +
77+
"directories for sphinx-mounts.",
78+
)
79+
4880
def _rewrite_needs_json_to_docs_sources(labels):
4981
"""Replace '@repo//:needs_json' -> '@repo//:docs_sources' for every item."""
5082
out = []
@@ -125,7 +157,34 @@ def _missing_requirements(deps):
125157
fail(msg)
126158
fail("This case should be unreachable?!")
127159

128-
def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = None, metamodel = None):
160+
def mount(label, mount_at, attach_to = None, entry_doc = "index", src_root = None):
161+
"""Declarative mount entry for the docs() macro.
162+
163+
Args:
164+
label: Bazel label producing a single output directory (typically a
165+
``files_to_dir`` target). Examples: ``"//src:docs_dir"``,
166+
``"@score_process//:docs_dir"``.
167+
mount_at: Docname prefix under which the bundle appears in the host
168+
Sphinx project. Example: ``"_mounted/internal"``.
169+
attach_to: Optional host docname whose toctree should receive the
170+
bundle's entry_doc.
171+
entry_doc: Mount-relative docname of the bundle's entry document.
172+
Defaults to ``"index"``.
173+
src_root: Optional in-repo source directory for the bundle (e.g.
174+
``"src/docs"``). When set, a ``ubproject.toml`` is generated at
175+
``<workspace>/<src_root>/`` during ``bazel run //:docs_check`` so
176+
ubCode and similar IDE extensions can resolve the project's type
177+
system when opening files inside the bundle.
178+
"""
179+
return struct(
180+
label = label,
181+
mount_at = mount_at,
182+
attach_to = attach_to,
183+
entry_doc = entry_doc,
184+
src_root = src_root,
185+
)
186+
187+
def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good = None, metamodel = None, mounts = []):
129188
"""Creates all targets related to documentation.
130189
131190
By using this function, you'll get any and all updates for documentation targets in one place.
@@ -138,6 +197,8 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
138197
known_good: Optional label to a "known good" JSON file for source links.
139198
metamodel: Optional label to a metamodel.yaml file. When set, the extension loads this
140199
file instead of the default metamodel shipped with score_metamodel.
200+
mounts: List of mount() entries describing documentation bundles to overlay into
201+
this project's Sphinx source tree.
141202
"""
142203

143204
call_path = native.package_name()
@@ -153,6 +214,18 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
153214
metamodel_env = {"SCORE_METAMODEL_YAML": "$(location " + str(metamodel) + ")"}
154215
metamodel_opts = ["--define=score_metamodel_yaml=$(location " + str(metamodel) + ")"]
155216

217+
mounts_payload = json.encode([
218+
{
219+
"label": m.label,
220+
"mount_at": m.mount_at,
221+
"attach_to": m.attach_to,
222+
"entry_doc": m.entry_doc,
223+
"src_root": m.src_root,
224+
}
225+
for m in mounts
226+
]) if mounts else ""
227+
mount_labels = [m.label for m in mounts]
228+
156229
module_deps = deps
157230
deps = deps + _missing_requirements(deps)
158231
deps = deps + [
@@ -163,7 +236,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
163236
sphinx_build_binary(
164237
name = "sphinx_build",
165238
visibility = ["//visibility:private"],
166-
data = data + metamodel_data,
239+
data = data + metamodel_data + mount_labels,
167240
deps = deps,
168241
)
169242

@@ -198,17 +271,19 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
198271
data_with_docs_sources = _rewrite_needs_json_to_docs_sources(data)
199272
additional_combo_sourcelinks = _rewrite_needs_json_to_sourcelinks(data)
200273
_merge_sourcelinks(name = "merged_sourcelinks", sourcelinks = [":sourcelinks_json"] + additional_combo_sourcelinks, known_good = known_good)
201-
docs_data = data + metamodel_data + [":sourcelinks_json"]
202-
combo_data = data_with_docs_sources + metamodel_data + [":merged_sourcelinks"]
274+
docs_data = data + metamodel_data + [":sourcelinks_json"] + mount_labels
275+
combo_data = data_with_docs_sources + metamodel_data + [":merged_sourcelinks"] + mount_labels
203276

204277
docs_env = {
205278
"SOURCE_DIRECTORY": source_dir,
206279
"DATA": str(data),
280+
"MOUNTS": mounts_payload,
207281
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
208282
} | metamodel_env
209283
docs_sources_env = {
210284
"SOURCE_DIRECTORY": source_dir,
211285
"DATA": str(data_with_docs_sources),
286+
"MOUNTS": mounts_payload,
212287
"SCORE_SOURCELINKS": "$(location :merged_sourcelinks)",
213288
} | metamodel_env
214289
if known_good:
@@ -304,18 +379,41 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = [], known_good =
304379
"--jobs",
305380
"auto",
306381
"--define=external_needs_source=" + str(data),
382+
"--define=mounts_source=" + mounts_payload,
307383
"--define=score_sourcelinks_json=$(location :sourcelinks_json)",
308384
"--define=score_source_code_linker_plain_links=1",
309385
],
310386
formats = ["needs"],
311387
sphinx = ":sphinx_build",
312-
tools = data + [":sourcelinks_json"],
388+
tools = data + [":sourcelinks_json"] + mount_labels,
313389
visibility = ["//visibility:public"],
314390
# Persistent workers cause stale symlinks after dependency version
315391
# changes, corrupting the Bazel cache.
316392
allow_persistent_workers = False,
317393
)
318394

395+
sphinx_docs(
396+
name = "docs_html",
397+
srcs = [":docs_sources"],
398+
config = ":" + source_prefix + "conf.py",
399+
extra_opts = [
400+
"-W",
401+
"--keep-going",
402+
"-T",
403+
"--jobs",
404+
"auto",
405+
"--define=external_needs_source=" + str(data),
406+
"--define=mounts_source=" + mounts_payload,
407+
"--define=score_sourcelinks_json=$(location :sourcelinks_json)",
408+
"--define=score_source_code_linker_plain_links=1",
409+
],
410+
formats = ["html"],
411+
sphinx = ":sphinx_build",
412+
tools = data + [":sourcelinks_json"] + mount_labels,
413+
visibility = ["//visibility:public"],
414+
allow_persistent_workers = False,
415+
)
416+
319417
native.alias(
320418
name = "traceability_gate",
321419
actual = "@score_docs_as_code//scripts_bazel:traceability_gate",

docs/how-to/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ Here you find practical guides on how to use docs-as-code.
3131
source_to_doc_links
3232
test_to_doc_links
3333
add_extensions
34+
mount_external_sources

0 commit comments

Comments
 (0)