Skip to content

Commit fc59863

Browse files
feat: support filegroups in code targets
1 parent 2c27741 commit fc59863

8 files changed

Lines changed: 116 additions & 64 deletions

File tree

bzl/bundle_rules.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ CodeTargetSourcesInfo = provider(
7575
)
7676

7777
def _source_files_from_attributes(ctx):
78-
"""Return files explicitly declared as source or header inputs by one rule."""
78+
"""Return files explicitly declared through source or header attributes."""
7979
source_files = []
8080
for attribute_name in ["srcs", "hdrs", "textual_hdrs"]:
8181
if not hasattr(ctx.rule.attr, attribute_name):
@@ -371,7 +371,7 @@ def _code_targets_sourcelinks_impl(ctx):
371371
for target in ctx.attr.code_targets
372372
])
373373
if not source_files.to_list():
374-
fail("code_targets must declare source files through srcs, hdrs, or textual_hdrs")
374+
fail("code_targets must declare source files through filegroups, srcs, hdrs, or textual_hdrs")
375375

376376
output = ctx.actions.declare_file(ctx.label.name + ".json")
377377
arguments = ctx.actions.args()

docs.bzl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ def docs_bundle(name, source_dir = None, entry_doc = "index", bundles = [], scan
7979
}.
8080
scan_code: Deprecated. Explicit source files or filegroups to scan for
8181
source-code links. Use `code_targets` for implementation targets.
82-
code_targets: Implementation targets to scan for source-code links. Their
83-
source files and the source files of their dependencies are
84-
collected recursively.
82+
code_targets: Implementation targets or filegroups to scan for source-code
83+
links. Implementation target source files and their dependencies
84+
are collected recursively; filegroups expand to their files.
8585
visibility: Target visibility.
8686
**kwargs: Additional attributes forwarded to the underlying rule.
8787
"""
@@ -169,7 +169,9 @@ def docs(
169169
deps: Additional dependencies for the documentation build.
170170
scan_code: Deprecated. Explicit source files or filegroups to scan for source
171171
code links. Use `code_targets` for implementation targets.
172-
code_targets: Implementation targets to scan recursively for source code links.
172+
code_targets: Implementation targets or filegroups to scan for source code
173+
links. Implementation targets are scanned recursively; filegroups
174+
expand to their files.
173175
test_sources: Optional list of repo-relative directory paths which will be used to filter testcases for documentation generation.
174176
When empty (default), all testcases found in `bazel-testlogs` will be used.
175177
known_good: Optional label to a "known good" JSON file for source links.

docs/how-to/source_to_doc_links.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ In your ``BUILD`` files, pass the implementation targets to the ``docs`` rule
4343
as ``code_targets``. Their ``srcs``, ``hdrs``, and ``textual_hdrs`` are scanned,
4444
including those of their ``deps`` recursively. This means that a
4545
``cc_executable`` also covers source files from the ``cc_library`` targets it
46-
uses.
46+
uses. You may also pass filegroups; their files are scanned directly.
4747

4848
.. code-block:: starlark
4949
:emphasize-lines: 14

docs/reference/bazel_macros.rst

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ Minimal example (root ``BUILD``)
7373
this function adds its own (but checks for conflicts).
7474

7575
- ``code_targets`` (list of Bazel labels)
76-
Implementation targets to scan for traceability tags (``req-Id:`` annotations).
77-
Their declared ``srcs``, ``hdrs``, and ``textual_hdrs`` are collected through
78-
their ``deps`` recursively. Each documentation bundle produces one cache for
79-
its declared targets; Bazel reuses that cache while its inputs are unchanged.
80-
The generated JSON is supplied to ``live_preview`` just like a normal
81-
documentation build.
76+
Implementation targets or filegroups to scan for traceability tags
77+
(``req-Id:`` annotations). Implementation target ``srcs``, ``hdrs``, and
78+
``textual_hdrs`` are collected through their ``deps`` recursively; filegroups
79+
expand to their files. Each documentation bundle produces one cache for its
80+
declared targets; Bazel reuses that cache while its inputs are unchanged. The
81+
generated JSON is supplied to ``live_preview`` just like a normal documentation
82+
build.
8283

8384
- ``scan_code`` (list of Bazel labels, deprecated)
8485
Explicit source files or filegroups to scan. Use ``code_targets`` for
@@ -171,11 +172,11 @@ Signature: ``docs_bundle(name, source_dir = None, entry_doc = "index", bundles =
171172
changing its canonical entry page.
172173

173174
- ``code_targets`` (list of Bazel labels, optional)
174-
Implementation targets whose ``srcs``, ``hdrs``, and ``textual_hdrs`` are
175-
scanned for requirement tags. Sources from their ``deps`` are included
176-
recursively, so declaring a ``cc_executable`` also scans the libraries it
177-
uses. The bundle owns one cached scan result; Bazel only regenerates it when
178-
its collected source inputs change.
175+
Implementation targets or filegroups to scan for requirement tags.
176+
Implementation target ``srcs``, ``hdrs``, and ``textual_hdrs`` are collected
177+
recursively from their ``deps``; filegroups expand to their files. The bundle
178+
owns one cached scan result; Bazel only regenerates it when its collected source
179+
inputs change.
179180

180181
- ``scan_code`` (list of Bazel labels, deprecated)
181182
Explicit source files or filegroups to scan. Prefer ``code_targets`` for

scripts_bazel/merge_sourcelinks.py

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,70 @@
2020
import logging
2121
import sys
2222
from pathlib import Path
23+
from typing import cast
2324

2425
from src.extensions.score_source_code_linker.helpers import parse_info_from_known_good
2526

2627
logging.basicConfig(level=logging.INFO, format="%(message)s")
2728
logger = logging.getLogger(__name__)
2829

2930

31+
def _reference_key(reference: dict[str, object]) -> str:
32+
"""Return a stable, hashable representation of one source-link reference."""
33+
# References are dictionaries and therefore cannot be added to ``seen`` directly.
34+
# Sorting their JSON keys makes semantically identical dictionaries produce the
35+
# same key even when their input key order differs.
36+
return json.dumps(reference, sort_keys=True)
37+
38+
39+
def _merge_sourcelinks_file(
40+
json_file: Path,
41+
known_good: Path | None,
42+
merged: list[dict[str, object]],
43+
seen: set[str],
44+
) -> None:
45+
"""Add the unique references from one sourcelinks JSON file to ``merged``."""
46+
with open(json_file, encoding="utf-8") as file:
47+
data = cast(list[object], json.load(file))
48+
if not data:
49+
return
50+
51+
raw_metadata = data[0]
52+
if not isinstance(raw_metadata, dict) or "repo_name" not in raw_metadata:
53+
logger.warning(
54+
f"Unexpected schema in sourcelinks file '{json_file}': "
55+
"expected first element to be a metadata dict "
56+
"with a 'repo_name' key. "
57+
)
58+
return
59+
metadata = cast(dict[str, object], raw_metadata)
60+
repo_name = metadata["repo_name"]
61+
if not isinstance(repo_name, str):
62+
logger.warning(
63+
f"Unexpected schema in sourcelinks file '{json_file}': "
64+
"expected metadata 'repo_name' to be a string. "
65+
)
66+
return
67+
68+
# A known-good file is optional for standalone builds that include
69+
# documentation from external modules. In that case, keep the metadata
70+
# produced by the individual sourcelinks file.
71+
if known_good and repo_name and repo_name != "local_repo":
72+
hash, repo = parse_info_from_known_good(
73+
known_good_json=known_good, repo_name=repo_name
74+
)
75+
metadata["hash"] = hash
76+
metadata["url"] = repo
77+
78+
for raw_reference in data[1:]:
79+
reference = cast(dict[str, object], raw_reference)
80+
reference.update(metadata)
81+
key = _reference_key(reference)
82+
if key not in seen:
83+
seen.add(key)
84+
merged.append(reference)
85+
86+
3087
def main():
3188
parser = argparse.ArgumentParser(
3289
description="Merge multiple sourcelinks JSON files into one"
@@ -39,6 +96,7 @@ def main():
3996
)
4097
_ = parser.add_argument(
4198
"--known_good",
99+
type=Path,
42100
help="Path to a required 'known good' JSON file (provided by Bazel).",
43101
)
44102
_ = parser.add_argument(
@@ -49,52 +107,15 @@ def main():
49107
)
50108

51109
args = parser.parse_args()
52-
all_files = [x for x in args.files if "known_good.json" not in str(x)]
53-
54-
merged = []
55-
seen = set()
56-
for json_file in all_files:
57-
with open(json_file) as f:
58-
data = json.load(f)
59-
# If the file is empty e.g. '[]' there is nothing to parse, we continue
60-
if not data:
61-
continue
62-
metadata = data[0]
63-
if not isinstance(metadata, dict) or "repo_name" not in metadata:
64-
logger.warning(
65-
f"Unexpected schema in sourcelinks file '{json_file}': "
66-
"expected first element to be a metadata dict "
67-
"with a 'repo_name' key. "
68-
)
69-
# As we can't deal with bad JSON structure we just skip it
70-
continue
71-
# A known-good file is optional for standalone builds that include
72-
# documentation from external modules. In that case, keep the
73-
# metadata produced by the individual sourcelinks file.
74-
if (
75-
args.known_good
76-
and metadata["repo_name"]
77-
and metadata["repo_name"] != "local_repo"
78-
):
79-
hash, repo = parse_info_from_known_good(
80-
known_good_json=args.known_good, repo_name=metadata["repo_name"]
81-
)
82-
metadata["hash"] = hash
83-
metadata["url"] = repo
84-
# In the case that 'metadata[repo_name]' is 'local_module'
85-
# hash & url are already existing and empty inside of 'metadata'
86-
# Therefore all 3 keys will be written to needlinks in each branch
87-
88-
for d in data[1:]:
89-
d.update(metadata)
90-
assert isinstance(data, list), repr(data)
91-
for reference in data[1:]:
92-
key = json.dumps(reference, sort_keys=True)
93-
if key not in seen:
94-
seen.add(key)
95-
merged.append(reference)
96-
with open(args.output, "w") as f:
97-
json.dump(merged, f, indent=2, ensure_ascii=False)
110+
111+
merged: list[dict[str, object]] = []
112+
seen: set[str] = set()
113+
for json_file in args.files:
114+
if "known_good.json" not in str(json_file):
115+
_merge_sourcelinks_file(json_file, args.known_good, merged, seen)
116+
117+
with open(args.output, "w", encoding="utf-8") as file:
118+
json.dump(merged, file, indent=2, ensure_ascii=False)
98119

99120
logger.info(f"Merged {len(args.files)} files into {len(merged)} total references")
100121
return 0

src/tests/docs_bzl/scenarios/nested_bundles/BUILD

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,22 @@ docs_bundle(
2222
code_targets = [
2323
":example_binary",
2424
":example_executable",
25+
":nested_filegroup_sources",
2526
],
2627
)
2728

29+
filegroup(
30+
name = "filegroup_sources",
31+
srcs = ["child/filegroup_source.py"],
32+
)
33+
34+
# Intentionally reference the same file through a second filegroup to verify
35+
# that code_targets resolves nested filegroups without duplicate links.
36+
filegroup(
37+
name = "nested_filegroup_sources",
38+
srcs = [":filegroup_sources"],
39+
)
40+
2841
py_binary(
2942
name = "example_binary",
3043
srcs = ["child/example.py"],
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# req-traceability: REQ_FILEGROUP

src/tests/docs_bzl/test_nested_bundles.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def test_nested_bundles_render_and_preserve_metadata():
5353
assert {link["file"] for link in sourcelinks} == {
5454
"src/tests/docs_bzl/scenarios/nested_bundles/child/example.py",
5555
"src/tests/docs_bzl/scenarios/nested_bundles/child/example.cc",
56+
"src/tests/docs_bzl/scenarios/nested_bundles/child/filegroup_source.py",
5657
}
5758
assert (result.build_dir / "concepts" / "example_bundle" / "index.html").is_file()
5859
assert (

0 commit comments

Comments
 (0)