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
38 changes: 34 additions & 4 deletions bzl/bundle_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ CodeTargetSourcesInfo = provider(
doc = "Source files collected from an implementation target and its dependencies.",
fields = {
"sources": "Depset of direct and transitive source files.",
"kind": "Bazel rule kind of the target selected by code_targets.",
},
)

Expand Down Expand Up @@ -101,6 +102,7 @@ def _collect_code_target_sources_impl(target, ctx):
direct = _source_files_from_attributes(ctx),
transitive = dependency_sources,
),
kind = ctx.rule.kind,
)]

_collect_code_target_sources = aspect(
Expand Down Expand Up @@ -167,6 +169,7 @@ def _rebase_bundle_entry(entry, mount_at, attach_to, data):
external = entry.external,
repository = entry.repository,
data = data,
code_targets = entry.code_targets,
)

def _entries_visible_through(ctx, child):
Expand Down Expand Up @@ -213,6 +216,13 @@ def _docs_bundle_impl(ctx):
own_source_files = []
own_external_runfiles = []
own_data = depset(direct = ctx.files.data)
own_code_targets = [
struct(
label = _format_bazel_label(target.label),
type = target[CodeTargetSourcesInfo].kind,
)
for target in ctx.attr.code_targets
]

if ctx.files.srcs:
runtime_path = _bundle_runtime_path(ctx)
Expand All @@ -229,6 +239,7 @@ def _docs_bundle_impl(ctx):
external = external,
repository = ctx.label.workspace_name,
data = own_data,
code_targets = own_code_targets,
))
own_source_files.extend(ctx.files.srcs)
# Local sources are read directly from the workspace by ``bazel run``.
Expand All @@ -246,6 +257,7 @@ def _docs_bundle_impl(ctx):
external = False,
repository = ctx.label.workspace_name,
data = own_data,
code_targets = own_code_targets,
))

child_source_files = []
Expand Down Expand Up @@ -305,11 +317,12 @@ _docs_bundle = rule(
"bundle_mount_ats": attr.string_list(),
"bundle_attach_tos": attr.string_list(),
"data": attr.label_list(allow_files = True),
"code_targets": attr.label_list(aspects = [_collect_code_target_sources]),
},
doc = "Internal rule that carries bundle files and their documentation-tree locations.",
)

def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "", entry_doc = "index", data = [], visibility = None, **kwargs):
def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "", entry_doc = "index", data = [], code_targets = [], visibility = None, **kwargs):
"""Create a reusable documentation bundle from files and child declarations."""
parsed_bundles = [_parse_bundle_declaration(declaration) for declaration in bundles]
_docs_bundle(
Expand All @@ -322,6 +335,7 @@ def create_bundle(name, bundles, srcs = [], sourcelinks = [], strip_prefix = "",
bundle_mount_ats = [bundle.mount_at for bundle in parsed_bundles],
bundle_attach_tos = [bundle.attach_to for bundle in parsed_bundles],
data = data,
code_targets = code_targets,
visibility = visibility,
**kwargs
)
Expand Down Expand Up @@ -400,17 +414,28 @@ def _code_targets_sourcelinks_impl(ctx):
target[CodeTargetSourcesInfo].sources
for target in ctx.attr.code_targets
])
if not source_files.to_list():
fail("code_targets must declare source files through filegroups, srcs, hdrs, or textual_hdrs")

output = ctx.actions.declare_file(ctx.label.name + ".json")
target_map = ctx.actions.declare_file(ctx.label.name + "_targets.json")
target_map_entries = []
for target in ctx.attr.code_targets:
target_info = target[CodeTargetSourcesInfo]
for source in target_info.sources.to_list():
target_map_entries.append({
"file": source.path,
"bazel_target": _format_bazel_label(target.label),
"bazel_type": target_info.kind,
})
ctx.actions.write(target_map, json.encode(target_map_entries))

arguments = ctx.actions.args()
arguments.add("--output", output.path)
arguments.add("--target-map", target_map.path)
arguments.add_all(source_files)
ctx.actions.run(
executable = ctx.executable._generate_sourcelinks,
arguments = [arguments],
inputs = source_files,
inputs = depset(direct = [target_map], transitive = [source_files]),
outputs = [output],
mnemonic = "GenerateCodeTargetSourcelinks",
)
Expand All @@ -437,3 +462,8 @@ def generate_code_target_sourcelinks(name, code_targets, visibility = None):
visibility = visibility,
)
return ":" + name

def _format_bazel_label(label):
"""Return an apparent-style label without Bzlmod's internal ``@@`` prefix."""
result = "//" + label.package + ":" + label.name
return "@" + label.workspace_name + result if label.workspace_name else result
26 changes: 26 additions & 0 deletions bzl/mount_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,29 @@ def create_mounts_manifest(name, bundle):
bundle = bundle,
)
return ":" + name

def _bundle_target_manifest_impl(ctx):
"""Write the code targets associated with each documentation-tree subtree."""
mappings = []
for entry in ctx.attr.bundle[DocsBundleInfo].entries:
if entry.code_targets:
mappings.append({
"mount_at": entry.mount_at,
"targets": [
{"bazel_target": target.label, "bazel_type": target.type}
for target in entry.code_targets
],
})
out = ctx.actions.declare_file(ctx.label.name + ".json")
ctx.actions.write(out, json.encode({"mappings": mappings}))
return [DefaultInfo(files = depset([out]))]

_bundle_target_manifest = rule(
implementation = _bundle_target_manifest_impl,
attrs = {"bundle": attr.label(providers = [DocsBundleInfo])},
doc = "Writes Bazel target metadata associated with documentation bundles.",
)

def create_bundle_target_manifest(name, bundle):
_bundle_target_manifest(name = name, bundle = bundle)
return ":" + name
12 changes: 10 additions & 2 deletions docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ load(
load(
"@score_docs_as_code//:bzl/mount_rules.bzl",
"create_mounts_manifest",
"create_bundle_target_manifest",
)

def _generated_conf_impl(ctx):
Expand Down Expand Up @@ -142,6 +143,7 @@ def docs_bundle(name, source_dir = None, data = [], entry_doc = "index", bundles
entry_doc = entry_doc,
bundles = bundles,
data = data,
code_targets = code_targets,
visibility = visibility,
**kwargs
)
Expand Down Expand Up @@ -294,6 +296,10 @@ def docs(
bundle = ":docs_bundle",
known_good = known_good,
)
create_bundle_target_manifest(
name = "bundle_target_manifest",
bundle = ":docs_bundle",
)

external_docs_runfiles(
name = "_external_docs_runfiles",
Expand All @@ -306,7 +312,7 @@ def docs(
# bundles do need runfiles, so keep only those sources.
docs_data = (
data + external_needs + metamodel_label +
[":sourcelinks_json", ":_external_docs_runfiles"] +
[":sourcelinks_json", ":bundle_target_manifest", ":_external_docs_runfiles"] +
mounts_manifest_label
)
if config_is_generated:
Expand All @@ -324,6 +330,7 @@ def docs(
# resolved by score_mounts through ``RUNFILES_DIR``.
"MOUNTS_MANIFEST": "$(rlocationpath :_mounts_manifest)" if bundles else "",
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
"SCORE_BAZEL_TARGETS": "$(rlocationpath :bundle_target_manifest)",
}
if config_is_generated:
# The generated file is named conf.py. Run targets pass its containing
Expand Down Expand Up @@ -408,6 +415,7 @@ def docs(
"auto",
"--define=external_needs_source=" + str(data + external_needs),
"--define=score_sourcelinks_json=$(location :sourcelinks_json)",
"--define=score_bazel_targets=$(location :bundle_target_manifest)",
"--define=score_source_code_linker_plain_links=1",
] + (
# ``sphinx_docs`` is a sandboxed build action, so it needs the
Expand All @@ -416,7 +424,7 @@ def docs(
) + (["--define=score_metamodel_yaml=$(location " + str(metamodel) + ")"] if metamodel else []),
formats = ["needs"],
sphinx = ":sphinx_build",
tools = data + external_needs + metamodel_label + [":sourcelinks_json", ":docs_bundle"] + mounts_manifest_label,
tools = data + external_needs + metamodel_label + [":sourcelinks_json", ":bundle_target_manifest", ":docs_bundle"] + mounts_manifest_label,
visibility = ["//visibility:public"],
# Persistent workers cause stale symlinks after dependency version
# changes, corrupting the Bazel cache.
Expand Down
7 changes: 6 additions & 1 deletion docs/reference/bazel_macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,15 @@ Signature: ``docs_bundle(name, source_dir = None, entry_doc = "index", bundles =
same bundle be mounted at different locations by different consumers without
changing its canonical entry page.

- ``code_targets`` (list of Bazel labels, optional)
- ``code_targets`` (list of Bazel labels, optional). For every source-code link
found in these targets, the corresponding need receives ``bazel_target`` (the
target label) and ``bazel_type`` (for example ``cc_library``).
Implementation targets or filegroups to scan for requirement tags.
Implementation target ``srcs``, ``hdrs``, and ``textual_hdrs`` are collected
recursively from their ``deps``; filegroups expand to their files. The bundle
may also reference an empty target (for example a component template before
implementation starts); its documentation needs still receive the target
metadata.
owns one cached scan result; Bazel only regenerates it when its collected source
inputs change.

Expand Down
24 changes: 24 additions & 0 deletions scripts_bazel/generate_sourcelinks_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""

import argparse
import json
import logging
import sys
from pathlib import Path
Expand Down Expand Up @@ -51,6 +52,19 @@ def clean_external_prefix(path: Path) -> Path:
return Path("".join(filepath_split[1:]))


def _load_target_map(path: Path | None) -> dict[str, list[tuple[str, str]]]:
"""Return the direct ``code_targets`` owning each scanned source file."""
if path is None:
return {}
entries = json.loads(path.read_text(encoding="utf-8"))
target_map: dict[str, list[tuple[str, str]]] = {}
for entry in entries:
target_map.setdefault(entry["file"], []).append(
(entry["bazel_target"], entry["bazel_type"])
)
return target_map


def main():
parser = argparse.ArgumentParser(
description="Generate source code links JSON from source files"
Expand All @@ -61,6 +75,11 @@ def main():
type=Path,
help="Output JSON file path",
)
_ = parser.add_argument(
"--target-map",
type=Path,
help="JSON mapping source paths to their Bazel target labels and rule kinds",
)
_ = parser.add_argument(
"files",
nargs="*",
Expand All @@ -71,6 +90,7 @@ def main():
args = parser.parse_args()

all_need_references = []
target_map = _load_target_map(args.target_map)

metadata = DefaultMetaData()
metadata_set = False
Expand All @@ -84,6 +104,10 @@ def main():
references = _extract_references_from_file(
abs_file_path.parent, Path(abs_file_path.name), clean_path
)
target_data = target_map.get(str(file_path), [])
for reference in references:
reference.bazel_target = ", ".join(target for target, _ in target_data)
reference.bazel_type = ", ".join(kind for _, kind in target_data)
all_need_references.extend(references)
store_source_code_links_with_metadata_json(
file=args.output, metadata=metadata, needlist=all_need_references
Expand Down
38 changes: 38 additions & 0 deletions scripts_bazel/tests/generate_sourcelinks_cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,44 @@ def test_generate_sourcelinks_cli_parses_cpp_traceability_tag(
assert data[1]["need"] == "tool_req__docs_arch_types"


def test_generate_sourcelinks_cli_adds_bazel_target_metadata(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
test_file = tmp_path / "test_source.cc"
test_file.write_text("// req-Id: tool_req__docs_arch_types\n")
target_map = tmp_path / "targets.json"
target_map.write_text(
json.dumps(
[
{
"file": str(test_file),
"bazel_target": "//components/filesystem:filesystem",
"bazel_type": "cc_library",
}
]
)
)
output_file = tmp_path / "output.json"

monkeypatch.setattr(
sys,
"argv",
[
str(_MY_PATH.parent / "generate_sourcelinks_cli.py"),
"--output",
str(output_file),
"--target-map",
str(target_map),
str(test_file),
],
)

assert scripts_bazel.generate_sourcelinks_cli.main() == 0
data = json.loads(output_file.read_text())
assert data[1]["bazel_target"] == "//components/filesystem:filesystem"
assert data[1]["bazel_type"] == "cc_library"


def test_generate_sourcelinks_cli_parse_external_module(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
Expand Down
2 changes: 2 additions & 0 deletions src/extensions/score_metamodel/metamodel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ needs_types_base_options:
# req-Id: tool_req__docs_dd_link_source_code_link
source_code_link: ^https://github.com/.*
testlink: ^https://github.com/.*
bazel_target: ^.*$
bazel_type: ^.*$
# Version will be mandatory global option in future releases
# For now giving grace periods to consumers
mandatory_options:
Expand Down
Loading
Loading