From d1dfc0c6fcb966379d64cc4d1c6daac673627d7e Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Wed, 21 Jan 2026 16:01:57 +0100 Subject: [PATCH 1/7] Provide //:sourcelinks_json --- BUILD | 19 ++++- docs.bzl | 26 +++++++ scripts/BUILD | 25 +++++++ scripts/generate_sourcelinks_cli.py | 72 +++++++++++++++++++ src/BUILD | 28 ++++++-- src/extensions/score_draw_uml_funcs/BUILD | 10 ++- src/extensions/score_header_service/BUILD | 12 ++-- src/extensions/score_layout/BUILD | 11 +-- src/extensions/score_metamodel/BUILD | 35 ++++++--- src/extensions/score_source_code_linker/BUILD | 48 ++++++------- src/extensions/score_sphinx_bundle/BUILD | 8 ++- src/extensions/score_sync_toml/BUILD | 12 ++-- src/find_runfiles/BUILD | 8 ++- src/helper_lib/BUILD | 11 +-- 14 files changed, 265 insertions(+), 60 deletions(-) create mode 100644 scripts/BUILD create mode 100644 scripts/generate_sourcelinks_cli.py diff --git a/BUILD b/BUILD index 21a92e427..4dc359350 100644 --- a/BUILD +++ b/BUILD @@ -13,7 +13,7 @@ load("@aspect_rules_py//py:defs.bzl", "py_library") load("@score_tooling//:defs.bzl", "cli_helper", "copyright_checker") -load("//:docs.bzl", "docs") +load("//:docs.bzl", "docs", "sourcelinks_json") package(default_visibility = ["//visibility:public"]) @@ -29,6 +29,23 @@ copyright_checker( visibility = ["//visibility:public"], ) +sourcelinks_json( + name = "sourcelinks_json", + srcs = [ + "//src:all_sources", + "//src/extensions/score_draw_uml_funcs:all_sources", + "//src/extensions/score_header_service:all_sources", + "//src/extensions/score_layout:all_sources", + "//src/extensions/score_metamodel:all_sources", + "//src/extensions/score_source_code_linker:all_sources", + "//src/extensions/score_sphinx_bundle:all_sources", + "//src/extensions/score_sync_toml:all_sources", + "//src/helper_lib:all_sources", + "//src/find_runfiles:all_sources", + ], + visibility = ["//visibility:public"], +) + docs( data = [ "@score_process//:needs_json", diff --git a/docs.bzl b/docs.bzl index 00f1c676c..91c66ef66 100644 --- a/docs.bzl +++ b/docs.bzl @@ -193,3 +193,29 @@ def docs(source_dir = "docs", data = [], deps = []): tools = data, visibility = ["//visibility:public"], ) + +def sourcelinks_json(name, srcs, visibility = None): + """ + Creates a target that generates a JSON file with source code links. + + See https://eclipse-score.github.io/docs-as-code/main/how-to/source_to_doc_links.html + + Args: + name: Name of the target + srcs: Source files to scan for traceability tags + visibility: Visibility of the target + """ + output_file = name + ".json" + + native.genrule( + name = name, + srcs = srcs, + outs = [output_file], + cmd = """ + $(location //scripts:generate_sourcelinks) \ + --output $@ \ + $(SRCS) + """, + tools = ["//scripts:generate_sourcelinks"], + visibility = visibility, + ) diff --git a/scripts/BUILD b/scripts/BUILD new file mode 100644 index 000000000..7acc067ef --- /dev/null +++ b/scripts/BUILD @@ -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 +# ******************************************************************************* + +load("@aspect_rules_py//py:defs.bzl", "py_binary") +load("@pip_process//:requirements.bzl", "all_requirements") + +py_binary( + name = "generate_sourcelinks", + srcs = ["generate_sourcelinks_cli.py"], + main = "generate_sourcelinks_cli.py", + visibility = ["//visibility:public"], + deps = [ + "//src/extensions/score_source_code_linker", + ] + all_requirements, +) diff --git a/scripts/generate_sourcelinks_cli.py b/scripts/generate_sourcelinks_cli.py new file mode 100644 index 000000000..a70b878a0 --- /dev/null +++ b/scripts/generate_sourcelinks_cli.py @@ -0,0 +1,72 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +""" +CLI tool to generate source code links JSON from source files. +This is used by the Bazel sourcelinks_json rule to create a JSON file +with all source code links for documentation needs. +""" + +import argparse +import logging +import sys +from pathlib import Path + +from src.extensions.score_source_code_linker.generate_source_code_links_json import ( + _extract_references_from_file, +) +from src.extensions.score_source_code_linker.needlinks import ( + store_source_code_links_json, +) + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate source code links JSON from source files" + ) + parser.add_argument( + "--output", + required=True, + type=Path, + help="Output JSON file path", + ) + parser.add_argument( + "files", + nargs="*", + type=Path, + help="Source files to scan for traceability tags", + ) + + args = parser.parse_args() + + all_need_references = [] + for file_path in args.files: + abs_file_path = file_path.resolve() + if abs_file_path.exists(): + references = _extract_references_from_file( + abs_file_path.parent, Path(abs_file_path.name) + ) + all_need_references.extend(references) + + store_source_code_links_json(args.output, all_need_references) + logger.info( + f"Found {len(all_need_references)} need references in {len(args.files)} files" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/BUILD b/src/BUILD index ddf506de1..fdcde729a 100644 --- a/src/BUILD +++ b/src/BUILD @@ -10,14 +10,11 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* + load("@aspect_rules_lint//format:defs.bzl", "format_multirun", "format_test") -load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_library") -load("@pip_process//:requirements.bzl", "all_requirements", "requirement") -load("@rules_pkg//pkg:mappings.bzl", "pkg_files") -load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@aspect_rules_py//py:defs.bzl", "py_library") load("@rules_python//python:pip.bzl", "compile_pip_requirements") -load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary") -load("@score_tooling//:defs.bzl", "dash_license_checker", "score_virtualenv") +load("@score_tooling//:defs.bzl", "dash_license_checker") # 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 @@ -28,6 +25,25 @@ exports_files( "requirements.txt", "incremental.py", "dummy.py", + "generate_sourcelinks_cli.py", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "all_sources", + srcs = glob( + ["*.py"] + ) + [ + "//src/extensions/score_draw_uml_funcs:all_sources", + "//src/extensions/score_header_service:all_sources", + "//src/extensions/score_layout:all_sources", + "//src/extensions/score_metamodel:all_sources", + "//src/extensions/score_source_code_linker:all_sources", + "//src/extensions/score_sphinx_bundle:all_sources", + "//src/extensions/score_sync_toml:all_sources", + "//src/helper_lib:all_sources", + "//src/find_runfiles:all_sources", ], visibility = ["//visibility:public"], ) diff --git a/src/extensions/score_draw_uml_funcs/BUILD b/src/extensions/score_draw_uml_funcs/BUILD index 21d8622e4..b16000a61 100644 --- a/src/extensions/score_draw_uml_funcs/BUILD +++ b/src/extensions/score_draw_uml_funcs/BUILD @@ -13,11 +13,15 @@ load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "all_requirements") +filegroup( + name = "all_sources", + srcs = glob(["*.py"]), + visibility = ["//visibility:public"], +) + py_library( name = "score_draw_uml_funcs", - srcs = glob( - ["*.py"], - ), + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], # TODO: Figure out if all requirements are needed or if we can break it down a bit diff --git a/src/extensions/score_header_service/BUILD b/src/extensions/score_header_service/BUILD index d7a7b65cc..481858112 100644 --- a/src/extensions/score_header_service/BUILD +++ b/src/extensions/score_header_service/BUILD @@ -10,16 +10,20 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* + load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") +filegroup( + name = "all_sources", + srcs = glob(["*.py"]), + visibility = ["//visibility:public"], +) + py_library( name = "score_header_service", - srcs = glob( - ["*.py"], - exclude = ["test/**"], - ), + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], # TODO: Figure out if all requirements are needed or if we can break it down a bit diff --git a/src/extensions/score_layout/BUILD b/src/extensions/score_layout/BUILD index d6d472746..03bbe1122 100644 --- a/src/extensions/score_layout/BUILD +++ b/src/extensions/score_layout/BUILD @@ -13,17 +13,20 @@ load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "requirement") +filegroup( + name = "all_sources", + srcs = glob(["*.py", "assets/**/*"]), + visibility = ["//visibility:public"], +) + py_library( name = "score_layout", - srcs = glob([ - "*.py", + srcs = [":all_sources"], # Adding assets as src instead of data ensures they are included in the # library as they would normally be, and we do not need to go through bazel's # RUNFILES_DIR mechanism to access them. This makes the code much simpler. # And it makes the library far easier extractable from bazel into a normal # python package if we ever want to do that. - "assets/**", - ]), imports = ["."], visibility = ["//visibility:public"], deps = [requirement("sphinx")], diff --git a/src/extensions/score_metamodel/BUILD b/src/extensions/score_metamodel/BUILD index 40cb645f4..40f1862f7 100644 --- a/src/extensions/score_metamodel/BUILD +++ b/src/extensions/score_metamodel/BUILD @@ -10,20 +10,39 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* + load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") -py_library( - name = "score_metamodel", +filegroup( + name = "sources", + srcs = glob( + ["*.py", "*.yaml", "*.json", "checks/*.py"] + ), +) + +filegroup( + name = "tests", srcs = glob( - ["**/*.py"], - ) + ["metamodel.yaml"], - data = glob(["*.yaml"]), # Needed to remove 'resolving of symlink' in score_metamodel.__init__ - imports = [ - ".", + ["tests/**/*"] + ), +) + +filegroup( + name = "all_sources", + srcs = [ + ":sources", + ":tests", ], visibility = ["//visibility:public"], +) + +py_library( + name = "score_metamodel", + srcs = [":sources"], + imports = ["."], + visibility = ["//visibility:public"], # TODO: Figure out if all requirements are needed or if we can break it down a bit deps = all_requirements + ["@score_docs_as_code//src/helper_lib"], ) @@ -31,7 +50,7 @@ py_library( score_py_pytest( name = "score_metamodel_tests", size = "small", - srcs = glob(["tests/*.py"]), + srcs = [":tests"], # All requirements already in the library so no need to have it double data = glob( [ diff --git a/src/extensions/score_source_code_linker/BUILD b/src/extensions/score_source_code_linker/BUILD index 7a662df3b..ac18574b6 100644 --- a/src/extensions/score_source_code_linker/BUILD +++ b/src/extensions/score_source_code_linker/BUILD @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation +# Copyright (c) 2026 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -10,28 +10,32 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -#******************************************************************************* -# Copyright (c) 2025 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 -# ******************************************************************************* -load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_library") -load("@pip_process//:requirements.bzl", "all_requirements") + +load("@aspect_rules_py//py:defs.bzl", "py_library") load("@score_tooling//:defs.bzl", "score_py_pytest") -py_library( - name = "score_source_code_linker", +filegroup( + name = "sources", + srcs = glob( + ["*.py"] + ), +) +filegroup( + name = "tests", srcs = glob( - ["**/*.py"], - exclude = ["tests/*.py"], + ["tests/*.py", "tests/*.json"] ), +) + +filegroup( + name = "all_sources", + srcs = [":sources", ":tests"], + visibility = ["//visibility:public"], +) + +py_library( + name = "score_source_code_linker", + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], deps = ["@score_docs_as_code//src/helper_lib"], @@ -39,11 +43,7 @@ py_library( py_library( name = "source_code_linker_helpers", - srcs = [ - "needlinks.py", - "testlink.py", - "xml_parser.py", - ], + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], ) diff --git a/src/extensions/score_sphinx_bundle/BUILD b/src/extensions/score_sphinx_bundle/BUILD index 10aa50a31..9145b5ab2 100644 --- a/src/extensions/score_sphinx_bundle/BUILD +++ b/src/extensions/score_sphinx_bundle/BUILD @@ -13,9 +13,15 @@ load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "all_requirements") +filegroup( + name = "all_sources", + srcs = glob(["*.py"]), + visibility = ["//visibility:public"], +) + py_library( name = "score_sphinx_bundle", - srcs = ["__init__.py"], + srcs = [":all_sources"], visibility = ["//visibility:public"], deps = all_requirements + [ "@score_docs_as_code//src/extensions:score_plantuml", diff --git a/src/extensions/score_sync_toml/BUILD b/src/extensions/score_sync_toml/BUILD index e9be69268..7b41f3b5d 100644 --- a/src/extensions/score_sync_toml/BUILD +++ b/src/extensions/score_sync_toml/BUILD @@ -10,15 +10,19 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* + load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "requirement") +filegroup( + name = "all_sources", + srcs = glob(["*.py", "*.toml"]), + visibility = ["//visibility:public"], +) + py_library( name = "score_sync_toml", - srcs = [ - "__init__.py", - "shared.toml", - ], + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], deps = [ diff --git a/src/find_runfiles/BUILD b/src/find_runfiles/BUILD index a286c57f2..2112815c4 100644 --- a/src/find_runfiles/BUILD +++ b/src/find_runfiles/BUILD @@ -14,9 +14,15 @@ load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") +filegroup( + name = "all_sources", + srcs = glob(["*.py"]), + visibility = ["//visibility:public"], +) + py_library( name = "find_runfiles", - srcs = ["__init__.py"], + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], ) diff --git a/src/helper_lib/BUILD b/src/helper_lib/BUILD index 61d941754..c2c3161f5 100644 --- a/src/helper_lib/BUILD +++ b/src/helper_lib/BUILD @@ -14,12 +14,15 @@ load("@aspect_rules_py//py:defs.bzl", "py_library") load("@pip_process//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") +filegroup( + name = "all_sources", + srcs = glob(["*.py"]), + visibility = ["//visibility:public"], +) + py_library( name = "helper_lib", - srcs = [ - "__init__.py", - "additional_functions.py", - ], + srcs = [":all_sources"], imports = ["."], visibility = ["//visibility:public"], deps = ["@score_docs_as_code//src/extensions/score_source_code_linker:source_code_linker_helpers"], From 8d9c7b2ed2822d8af1cde1c405a6fe8f126a06cd Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Thu, 22 Jan 2026 10:38:15 +0100 Subject: [PATCH 2/7] Funnel sourcelinks into Sphinx extension Currently, we are not using it yet, but this commit adds the Bazel scaffolding to make the json data available. --- BUILD | 1 + docs.bzl | 47 ++++++++++++-- scripts/BUILD | 7 ++ scripts/merge_sourcelinks.py | 64 +++++++++++++++++++ .../score_source_code_linker/__init__.py | 5 ++ 5 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 scripts/merge_sourcelinks.py diff --git a/BUILD b/BUILD index 4dc359350..a902c541d 100644 --- a/BUILD +++ b/BUILD @@ -51,6 +51,7 @@ docs( "@score_process//:needs_json", ], source_dir = "docs", + sourcelinks = [":sourcelinks_json"], ) cli_helper( diff --git a/docs.bzl b/docs.bzl index 91c66ef66..cef918e54 100644 --- a/docs.bzl +++ b/docs.bzl @@ -56,12 +56,40 @@ def _rewrite_needs_json_to_docs_sources(labels): out.append(s) return out -def docs(source_dir = "docs", data = [], deps = []): +def _merge_sourcelinks(name, sourcelinks): + """Merge multiple sourcelinks JSON files into a single file. + + Args: + name: Name for the merged sourcelinks target + sourcelinks: List of sourcelinks JSON file targets """ - Creates all targets related to documentation. + + native.genrule( + name = name, + srcs = sourcelinks, + outs = [name + ".json"], + cmd = """ + $(location //scripts:merge_sourcelinks) \ + --output $@ \ + $(SRCS) + """, + tools = ["//scripts:merge_sourcelinks"], + ) + +def docs(source_dir = "docs", data = [], deps = [], sourcelinks = []): + """Creates all targets related to documentation. + By using this function, you'll get any and all updates for documentation targets in one place. + + Args: + 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. + sourcelinks: Source code links configuration for traceability. """ + _merge_sourcelinks(name="merged_sourcelinks", sourcelinks=sourcelinks) + call_path = native.package_name() if call_path != "": @@ -106,12 +134,13 @@ def docs(source_dir = "docs", data = [], deps = []): name = "docs", tags = ["cli_help=Build documentation:\nbazel run //:docs"], srcs = ["@score_docs_as_code//src:incremental.py"], - data = data, + data = data + [":merged_sourcelinks"], deps = deps, env = { "SOURCE_DIRECTORY": source_dir, "DATA": str(data), "ACTION": "incremental", + "SCORE_SOURCELINKS": "$(location :merged_sourcelinks)", }, ) @@ -119,12 +148,13 @@ def docs(source_dir = "docs", data = [], deps = []): name = "docs_combo_experimental", tags = ["cli_help=Build full documentation with all dependencies:\nbazel run //:docs_combo_experimental"], srcs = ["@score_docs_as_code//src:incremental.py"], - data = data_with_docs_sources, + data = data_with_docs_sources + [":merged_sourcelinks"], deps = deps, env = { "SOURCE_DIRECTORY": source_dir, "DATA": str(data_with_docs_sources), "ACTION": "incremental", + "SCORE_SOURCELINKS": "$(location :merged_sourcelinks)", }, ) @@ -132,12 +162,13 @@ def docs(source_dir = "docs", data = [], deps = []): name = "docs_check", tags = ["cli_help=Verify documentation:\nbazel run //:docs_check"], srcs = ["@score_docs_as_code//src:incremental.py"], - data = data, + data = data + [":merged_sourcelinks"], deps = deps, env = { "SOURCE_DIRECTORY": source_dir, "DATA": str(data), "ACTION": "check", + "SCORE_SOURCELINKS": "$(location :merged_sourcelinks)", }, ) @@ -145,12 +176,13 @@ def docs(source_dir = "docs", data = [], deps = []): 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 = data, + data = data + [":merged_sourcelinks"], deps = deps, env = { "SOURCE_DIRECTORY": source_dir, "DATA": str(data), "ACTION": "live_preview", + "SCORE_SOURCELINKS": "$(location :merged_sourcelinks)", }, ) @@ -158,12 +190,13 @@ def docs(source_dir = "docs", data = [], deps = []): 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 = data_with_docs_sources, + data = data_with_docs_sources + [":merged_sourcelinks"], deps = deps, env = { "SOURCE_DIRECTORY": source_dir, "DATA": str(data_with_docs_sources), "ACTION": "live_preview", + "SCORE_SOURCELINKS": "$(location :merged_sourcelinks)", }, ) diff --git a/scripts/BUILD b/scripts/BUILD index 7acc067ef..ab9f0a9f6 100644 --- a/scripts/BUILD +++ b/scripts/BUILD @@ -23,3 +23,10 @@ py_binary( "//src/extensions/score_source_code_linker", ] + all_requirements, ) + +py_binary( + name = "merge_sourcelinks", + srcs = ["merge_sourcelinks.py"], + main = "merge_sourcelinks.py", + visibility = ["//visibility:public"], +) diff --git a/scripts/merge_sourcelinks.py b/scripts/merge_sourcelinks.py new file mode 100644 index 000000000..15e6e3b9c --- /dev/null +++ b/scripts/merge_sourcelinks.py @@ -0,0 +1,64 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* + +""" +Merge multiple sourcelinks JSON files into a single JSON file. +""" + +import argparse +import json +import logging +import sys +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser( + description="Merge multiple sourcelinks JSON files into one" + ) + parser.add_argument( + "--output", + required=True, + type=Path, + help="Output merged JSON file path", + ) + parser.add_argument( + "files", + nargs="*", + type=Path, + help="Input JSON files to merge", + ) + + args = parser.parse_args() + + merged = [] + for json_file in args.files: + if json_file.exists(): + with open(json_file, "r") as f: + data = json.load(f) + if isinstance(data, list): + merged.extend(data) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with open(args.output, "w") as f: + json.dump(merged, f, indent=2, ensure_ascii=False) + + logger.info(f"Merged {len(args.files)} files into {len(merged)} total references") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index 876e4fccc..39adb484f 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -20,6 +20,7 @@ # req-Id: tool_req__docs_dd_link_source_code_link # This whole directory implements the above mentioned tool requirements +import os from collections import defaultdict from copy import deepcopy from pathlib import Path @@ -175,6 +176,10 @@ def setup_source_code_linker(app: Sphinx, ws_root: Path): app.outdir, "score_source_code_linker_cache.json" ) + score_sourcelinks_json = os.environ.get("SCORE_SOURCELINKS") + if score_sourcelinks_json: + print(f"TODO: Use {score_sourcelinks_json} for source code linker") + if ( not scl_cache_json.exists() or not app.config.skip_rescanning_via_source_code_linker From e426bcdd93526365464d7c31f381dc9c72aebed7 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Thu, 22 Jan 2026 13:16:53 +0100 Subject: [PATCH 3/7] Read source links json when available --- .../score_source_code_linker/__init__.py | 21 +++++++++++++------ .../score_source_code_linker/needlinks.py | 7 +++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index 39adb484f..ed6548079 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -130,9 +130,13 @@ def build_and_save_combined_file(outdir: Path): Reads the saved partial caches of codelink & testlink Builds the combined JSON cache & saves it """ - source_code_links = load_source_code_links_json( - get_cache_filename(outdir, "score_source_code_linker_cache.json") - ) + source_code_links_json = os.environ.get("SCORE_SOURCELINKS") + if not source_code_links_json: + source_code_links_json = get_cache_filename(outdir, "score_source_code_linker_cache.json") + else: + source_code_links_json = Path(source_code_links_json) + + source_code_links = load_source_code_links_json(source_code_links_json) test_code_links = load_test_xml_parsed_json( get_cache_filename(outdir, "score_xml_parser_cache.json") ) @@ -172,13 +176,16 @@ def setup_source_code_linker(app: Sphinx, ws_root: Path): }, } + score_sourcelinks_json = os.environ.get("SCORE_SOURCELINKS") + if score_sourcelinks_json: + # No need to generate the JSON file if this env var is set + # because it points to an existing file with the needed data. + return + scl_cache_json = get_cache_filename( app.outdir, "score_source_code_linker_cache.json" ) - score_sourcelinks_json = os.environ.get("SCORE_SOURCELINKS") - if score_sourcelinks_json: - print(f"TODO: Use {score_sourcelinks_json} for source code linker") if ( not scl_cache_json.exists() @@ -200,6 +207,7 @@ def register_test_code_linker(app: Sphinx): def setup_test_code_linker(app: Sphinx, env: BuildEnvironment): + # TODO instead of implementing our own caching here, we should rely on Bazel tl_cache_json = get_cache_filename(app.outdir, "score_xml_parser_cache.json") if ( not tl_cache_json.exists() @@ -249,6 +257,7 @@ def register_combined_linker(app: Sphinx): def setup_combined_linker(app: Sphinx, _: BuildEnvironment): grouped_cache = get_cache_filename(app.outdir, "score_scl_grouped_cache.json") gruped_cache_exists = grouped_cache.exists() + # TODO this cache should be done via Bazel if not gruped_cache_exists or not app.config.skip_rescanning_via_source_code_linker: LOGGER.debug( "Did not find combined json 'score_scl_grouped_cache.json' in _build." diff --git a/src/extensions/score_source_code_linker/needlinks.py b/src/extensions/score_source_code_linker/needlinks.py index c890b13e4..348147292 100644 --- a/src/extensions/score_source_code_linker/needlinks.py +++ b/src/extensions/score_source_code_linker/needlinks.py @@ -13,6 +13,7 @@ # req-Id: tool_req__docs_dd_link_source_code_link import json +import os from dataclasses import asdict, dataclass from pathlib import Path from typing import Any @@ -80,6 +81,12 @@ def store_source_code_links_json(file: Path, needlist: list[NeedLink]): def load_source_code_links_json(file: Path) -> list[NeedLink]: + if not file.is_absolute(): + # use env variable set by Bazel + ws_root = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if ws_root: + file = Path(ws_root) / file + links: list[NeedLink] = json.loads( file.read_text(encoding="utf-8"), object_hook=needlink_decoder, From e2bfbf7eefc90b68c3178136b10e21ec90fa027e Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Thu, 22 Jan 2026 13:28:41 +0100 Subject: [PATCH 4/7] Fix CI checks --- BUILD | 2 +- docs.bzl | 2 +- scripts/merge_sourcelinks.py | 2 +- src/BUILD | 4 ++-- src/extensions/score_layout/BUILD | 15 +++++++++------ src/extensions/score_metamodel/BUILD | 9 +++++++-- src/extensions/score_source_code_linker/BUILD | 15 +++++++++++---- .../score_source_code_linker/__init__.py | 5 +++-- src/extensions/score_sync_toml/BUILD | 5 ++++- 9 files changed, 39 insertions(+), 20 deletions(-) diff --git a/BUILD b/BUILD index a902c541d..adbbd688a 100644 --- a/BUILD +++ b/BUILD @@ -40,8 +40,8 @@ sourcelinks_json( "//src/extensions/score_source_code_linker:all_sources", "//src/extensions/score_sphinx_bundle:all_sources", "//src/extensions/score_sync_toml:all_sources", - "//src/helper_lib:all_sources", "//src/find_runfiles:all_sources", + "//src/helper_lib:all_sources", ], visibility = ["//visibility:public"], ) diff --git a/docs.bzl b/docs.bzl index cef918e54..e65e7d00f 100644 --- a/docs.bzl +++ b/docs.bzl @@ -88,7 +88,7 @@ def docs(source_dir = "docs", data = [], deps = [], sourcelinks = []): sourcelinks: Source code links configuration for traceability. """ - _merge_sourcelinks(name="merged_sourcelinks", sourcelinks=sourcelinks) + _merge_sourcelinks(name = "merged_sourcelinks", sourcelinks = sourcelinks) call_path = native.package_name() diff --git a/scripts/merge_sourcelinks.py b/scripts/merge_sourcelinks.py index 15e6e3b9c..2d9141f22 100644 --- a/scripts/merge_sourcelinks.py +++ b/scripts/merge_sourcelinks.py @@ -47,7 +47,7 @@ def main(): merged = [] for json_file in args.files: if json_file.exists(): - with open(json_file, "r") as f: + with open(json_file) as f: data = json.load(f) if isinstance(data, list): merged.extend(data) diff --git a/src/BUILD b/src/BUILD index fdcde729a..f45f14fd7 100644 --- a/src/BUILD +++ b/src/BUILD @@ -33,7 +33,7 @@ exports_files( filegroup( name = "all_sources", srcs = glob( - ["*.py"] + ["*.py"], ) + [ "//src/extensions/score_draw_uml_funcs:all_sources", "//src/extensions/score_header_service:all_sources", @@ -42,8 +42,8 @@ filegroup( "//src/extensions/score_source_code_linker:all_sources", "//src/extensions/score_sphinx_bundle:all_sources", "//src/extensions/score_sync_toml:all_sources", - "//src/helper_lib:all_sources", "//src/find_runfiles:all_sources", + "//src/helper_lib:all_sources", ], visibility = ["//visibility:public"], ) diff --git a/src/extensions/score_layout/BUILD b/src/extensions/score_layout/BUILD index 03bbe1122..8e21188de 100644 --- a/src/extensions/score_layout/BUILD +++ b/src/extensions/score_layout/BUILD @@ -15,18 +15,21 @@ load("@pip_process//:requirements.bzl", "requirement") filegroup( name = "all_sources", - srcs = glob(["*.py", "assets/**/*"]), + srcs = glob([ + "*.py", + "assets/**/*", + ]), visibility = ["//visibility:public"], ) py_library( name = "score_layout", srcs = [":all_sources"], - # Adding assets as src instead of data ensures they are included in the - # library as they would normally be, and we do not need to go through bazel's - # RUNFILES_DIR mechanism to access them. This makes the code much simpler. - # And it makes the library far easier extractable from bazel into a normal - # python package if we ever want to do that. + # Adding assets as src instead of data ensures they are included in the + # library as they would normally be, and we do not need to go through bazel's + # RUNFILES_DIR mechanism to access them. This makes the code much simpler. + # And it makes the library far easier extractable from bazel into a normal + # python package if we ever want to do that. imports = ["."], visibility = ["//visibility:public"], deps = [requirement("sphinx")], diff --git a/src/extensions/score_metamodel/BUILD b/src/extensions/score_metamodel/BUILD index 40f1862f7..ea76fdcc8 100644 --- a/src/extensions/score_metamodel/BUILD +++ b/src/extensions/score_metamodel/BUILD @@ -18,14 +18,19 @@ load("@score_tooling//:defs.bzl", "score_py_pytest") filegroup( name = "sources", srcs = glob( - ["*.py", "*.yaml", "*.json", "checks/*.py"] + [ + "*.py", + "*.yaml", + "*.json", + "checks/*.py", + ], ), ) filegroup( name = "tests", srcs = glob( - ["tests/**/*"] + ["tests/**/*"], ), ) diff --git a/src/extensions/score_source_code_linker/BUILD b/src/extensions/score_source_code_linker/BUILD index ac18574b6..ab9b64a54 100644 --- a/src/extensions/score_source_code_linker/BUILD +++ b/src/extensions/score_source_code_linker/BUILD @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation +# Copyright (c) 2025 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -17,19 +17,26 @@ load("@score_tooling//:defs.bzl", "score_py_pytest") filegroup( name = "sources", srcs = glob( - ["*.py"] + ["*.py"], ), ) + filegroup( name = "tests", srcs = glob( - ["tests/*.py", "tests/*.json"] + [ + "tests/*.py", + "tests/*.json", + ], ), ) filegroup( name = "all_sources", - srcs = [":sources", ":tests"], + srcs = [ + ":sources", + ":tests", + ], visibility = ["//visibility:public"], ) diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index ed6548079..c9e57b97d 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -132,7 +132,9 @@ def build_and_save_combined_file(outdir: Path): """ source_code_links_json = os.environ.get("SCORE_SOURCELINKS") if not source_code_links_json: - source_code_links_json = get_cache_filename(outdir, "score_source_code_linker_cache.json") + source_code_links_json = get_cache_filename( + outdir, "score_source_code_linker_cache.json" + ) else: source_code_links_json = Path(source_code_links_json) @@ -186,7 +188,6 @@ def setup_source_code_linker(app: Sphinx, ws_root: Path): app.outdir, "score_source_code_linker_cache.json" ) - if ( not scl_cache_json.exists() or not app.config.skip_rescanning_via_source_code_linker diff --git a/src/extensions/score_sync_toml/BUILD b/src/extensions/score_sync_toml/BUILD index 7b41f3b5d..fdb8acb35 100644 --- a/src/extensions/score_sync_toml/BUILD +++ b/src/extensions/score_sync_toml/BUILD @@ -16,7 +16,10 @@ load("@pip_process//:requirements.bzl", "requirement") filegroup( name = "all_sources", - srcs = glob(["*.py", "*.toml"]), + srcs = glob([ + "*.py", + "*.toml", + ]), visibility = ["//visibility:public"], ) From 529deb8c543346e672ae98b83ffcee8c78265f69 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Thu, 22 Jan 2026 17:04:13 +0100 Subject: [PATCH 5/7] Fix tests --- MODULE.bazel | 2 +- src/extensions/score_metamodel/BUILD | 6 +++--- src/extensions/score_source_code_linker/BUILD | 14 +++++++++----- .../tests/test_codelink.py | 14 ++++++++++++-- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index c7f9c56aa..33c76fc9c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -104,7 +104,7 @@ bazel_dep(name = "score_process", version = "1.4.2") # Add Linter bazel_dep(name = "rules_multitool", version = "1.9.0") -bazel_dep(name = "score_tooling", version = "1.0.2") +bazel_dep(name = "score_tooling", version = "1.0.5") multitool_root = use_extension("@rules_multitool//multitool:extension.bzl", "multitool") use_repo(multitool_root, "actionlint_hub", "multitool", "ruff_hub", "shellcheck_hub", "yamlfmt_hub") diff --git a/src/extensions/score_metamodel/BUILD b/src/extensions/score_metamodel/BUILD index ea76fdcc8..cc0dd2484 100644 --- a/src/extensions/score_metamodel/BUILD +++ b/src/extensions/score_metamodel/BUILD @@ -30,7 +30,7 @@ filegroup( filegroup( name = "tests", srcs = glob( - ["tests/**/*"], + ["tests/*.py"], ), ) @@ -55,13 +55,13 @@ py_library( score_py_pytest( name = "score_metamodel_tests", size = "small", - srcs = [":tests"], + srcs = glob(["tests/*.py"]), # All requirements already in the library so no need to have it double data = glob( [ "tests/**/*.rst", "tests/**/*.yaml", - ], + ] + ["tests/rst/conf.py"], ), deps = [":score_metamodel"], ) diff --git a/src/extensions/score_source_code_linker/BUILD b/src/extensions/score_source_code_linker/BUILD index ab9b64a54..8dd7deb11 100644 --- a/src/extensions/score_source_code_linker/BUILD +++ b/src/extensions/score_source_code_linker/BUILD @@ -42,15 +42,19 @@ filegroup( py_library( name = "score_source_code_linker", - srcs = [":all_sources"], + srcs = [":sources"], imports = ["."], visibility = ["//visibility:public"], - deps = ["@score_docs_as_code//src/helper_lib"], + deps = ["//src/helper_lib"], ) py_library( name = "source_code_linker_helpers", - srcs = [":all_sources"], + srcs = [ + "needlinks.py", + "testlink.py", + "xml_parser.py", + ], imports = ["."], visibility = ["//visibility:public"], ) @@ -65,10 +69,10 @@ score_py_pytest( "-s", "-vv", ], - data = glob(["**/*.json"]), + data = glob(["tests/*.json"]), imports = ["."], deps = [ ":score_source_code_linker", - "@score_docs_as_code//src/extensions/score_metamodel", + "//src/extensions/score_metamodel", ], ) diff --git a/src/extensions/score_source_code_linker/tests/test_codelink.py b/src/extensions/score_source_code_linker/tests/test_codelink.py index 9e360d1a5..bf3d75615 100644 --- a/src/extensions/score_source_code_linker/tests/test_codelink.py +++ b/src/extensions/score_source_code_linker/tests/test_codelink.py @@ -21,9 +21,19 @@ import pytest from attribute_plugin import add_test_properties # type: ignore[import-untyped] -from sphinx_needs.data import NeedsMutable +from sphinx_needs.data import NeedsMutable, NeedsInfoType + + +def test_need(**kwargs: Any) -> NeedsInfoType: + """Convinience function to create a NeedsInfoType object with some defaults.""" + + kwargs.setdefault("id", "test_need") + kwargs.setdefault("docname", "docname") + kwargs.setdefault("doctype", "rst") + kwargs.setdefault("lineno", "42") + + return NeedsInfoType(**kwargs) -from src.extensions.score_metamodel.tests import need as test_need # Import the module under test # Note: You'll need to adjust these imports based on your actual module structure From 4b19f68cfbc5e5389b458d7131aeb39064745024 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Thu, 22 Jan 2026 17:30:52 +0100 Subject: [PATCH 6/7] Document sourcelinks_json how to --- docs/how-to/source_to_doc_links.rst | 61 ++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/docs/how-to/source_to_doc_links.rst b/docs/how-to/source_to_doc_links.rst index f36866a30..3763b6d71 100644 --- a/docs/how-to/source_to_doc_links.rst +++ b/docs/how-to/source_to_doc_links.rst @@ -2,14 +2,65 @@ Reference Docs in Source Code ============================= In your C++/Rust/Python source code, you want to reference requirements (needs). -The docs-as-code tool will create backlinks in the documentation. +The docs-as-code tool will create backlinks in the documentation in two steps: + +1. You add a special comment in your source code that references the need ID. +2. Scan for those comments and provide needs links to your documentation. + +For an example result, look at the attribute ``source_code_link`` +of :need:`tool_req__docs_common_attr_title`. + +Comments in Source Code +----------------------- Use a comment and start with ``req-Id:`` or ``req-traceability:`` followed by the need ID. .. code-block:: python - # req-Id: TOOL_REQ__EXAMPLE_ID - # req-traceability: TOOL_REQ__EXAMPLE_ID + # req-Id: TOOL_REQ__EXAMPLE_ID + # req-traceability: TOOL_REQ__EXAMPLE_ID -For an example, look at the attribute ``source_code_link`` -of :need:`tool_req__docs_common_attr_title`. +For other languages (C++, Rust, etc.), use the appropriate comment syntax. + +Scanning Source Code for Links +------------------------------ + +In you ``BUILD`` files, you specify which source files to scan +with ``filegroup`` or ``glob`` or whatever Bazel mechanism you prefer. +Then, you use the ``sourcelinks_json`` rule to scan those files. +Finally, pass the scan results to the ``docs`` rule as ``sourcelinks`` attribute. + + +.. code-block:: starlark + :emphasize-lines: 1, 12, 26 + :linenos: + + load("//:docs.bzl", "docs", "sourcelinks_json") + + filegroup( + name = "some_sources", + srcs = [ + "foo.py", + "bar.cpp", + "data.yaml", + ] + glob(["subdir/**/.py"]), + ) + + sourcelinks_json( + name = "my_source_links", + srcs = [ + ":some_sources", + "//src:all_sources", + # whatever + ], + ) + + docs( + data = [ + "@score_process//:needs_json", + ], + source_dir = "docs", + sourcelinks = [":my_source_links"], + ) + +Since the source links are Bazel targets, you can easily reference other modules as well. From ae2d4d77d64d58bd3c0cdf4a8355bdcea0e1ddb9 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Thu, 22 Jan 2026 17:38:59 +0100 Subject: [PATCH 7/7] Fix linter warnings --- .../tests/test_codelink.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/extensions/score_source_code_linker/tests/test_codelink.py b/src/extensions/score_source_code_linker/tests/test_codelink.py index bf3d75615..88794c05b 100644 --- a/src/extensions/score_source_code_linker/tests/test_codelink.py +++ b/src/extensions/score_source_code_linker/tests/test_codelink.py @@ -21,19 +21,7 @@ import pytest from attribute_plugin import add_test_properties # type: ignore[import-untyped] -from sphinx_needs.data import NeedsMutable, NeedsInfoType - - -def test_need(**kwargs: Any) -> NeedsInfoType: - """Convinience function to create a NeedsInfoType object with some defaults.""" - - kwargs.setdefault("id", "test_need") - kwargs.setdefault("docname", "docname") - kwargs.setdefault("doctype", "rst") - kwargs.setdefault("lineno", "42") - - return NeedsInfoType(**kwargs) - +from sphinx_needs.data import NeedsInfoType, NeedsMutable # Import the module under test # Note: You'll need to adjust these imports based on your actual module structure @@ -66,6 +54,17 @@ def test_need(**kwargs: Any) -> NeedsInfoType: """ +def test_need(**kwargs: Any) -> NeedsInfoType: + """Convinience function to create a NeedsInfoType object with some defaults.""" + + kwargs.setdefault("id", "test_need") + kwargs.setdefault("docname", "docname") + kwargs.setdefault("doctype", "rst") + kwargs.setdefault("lineno", "42") + + return NeedsInfoType(**kwargs) + + def encode_comment(s: str) -> str: return s.replace(" ", "-----", 1)