Skip to content

Commit 4fc8519

Browse files
Resolve breathe project input using bazel and sphinx parameter
This allows us to specify bazel targets as breathe doxygen input instead of searching for potential projects in filesystem directly.
1 parent 49e35a1 commit 4fc8519

3 files changed

Lines changed: 115 additions & 25 deletions

File tree

bazel/rules/rules_score/private/sphinx_module.bzl

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,39 @@ load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
1919
load("@rules_python//sphinxdocs/private:sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo")
2020
load("//bazel/rules/rules_score:providers.bzl", "SphinxModuleInfo", "SphinxNeedsInfo")
2121

22+
# Delimiter used to encode filter_execpath parameters into a single extra_opts string.
23+
_FILTER_EXECPATH_DELIM = "@@FILTER_EXECPATH@@"
24+
25+
def filter_execpath(flag, label, filter_pattern):
26+
"""Construct an extra_opts entry that filters execpaths and rewrites them with the relocated target prefix.
27+
28+
When used in extra_opts of sphinx_module, the rule implementation will:
29+
1. Expand $(execpaths <label>) to get all output paths
30+
2. Filter for the path containing <filter_pattern>
31+
3. Rewrite the path to include the target's relocated prefix
32+
(e.g. bazel-out/.../bin/<package>/<name>/<original_path>)
33+
34+
Example usage in BUILD:
35+
load("@score_tooling//bazel/rules/rules_score:rules_score.bzl", "filter_execpath", "sphinx_module")
36+
37+
sphinx_module(
38+
name = "sphinx_doc",
39+
extra_opts = [
40+
filter_execpath("-Dbreathe_projects.com", "//docs/sphinx:doxygen_xml", "doxygen_build/xml"),
41+
],
42+
...
43+
)
44+
45+
Args:
46+
flag: The Sphinx -D flag prefix (e.g. "-Dbreathe_projects.com")
47+
label: The Bazel label whose execpaths to expand (e.g. "//docs/sphinx:doxygen_xml")
48+
filter_pattern: Substring to match when filtering the execpaths (e.g. "doxygen_build/xml")
49+
50+
Returns:
51+
A specially formatted string that the sphinx_module rule will process.
52+
"""
53+
return _FILTER_EXECPATH_DELIM.join([flag, str(label), filter_pattern])
54+
2255
def _create_config_py(ctx):
2356
"""Get or generate the conf.py configuration file.
2457
@@ -113,6 +146,62 @@ def _score_html_impl(ctx):
113146
Phase 2: Generate HTML with external needs and merge all dependency HTML
114147
"""
115148

149+
run_args = [] # Copy of the args to forward along to debug runner
150+
args = ctx.actions.args() # Args passed to the action
151+
152+
# Expand location references in extra_opts and collect as sphinx arguments.
153+
# targets must include all labels referenced via $(location ...) / $(execpaths ...).
154+
extra_opts_targets = ctx.attr.srcs + ctx.attr.docs_library_deps
155+
source_prefix = ctx.label.name
156+
157+
for opt in ctx.attr.extra_opts:
158+
if _FILTER_EXECPATH_DELIM in opt:
159+
# Process filter_execpath() encoded strings:
160+
# Format: flag@@FILTER_EXECPATH@@label@@FILTER_EXECPATH@@filter_pattern
161+
parts = opt.split(_FILTER_EXECPATH_DELIM)
162+
flag = parts[0]
163+
label_str = parts[1]
164+
filter_pattern = parts[2]
165+
166+
# Expand execpaths for the referenced label
167+
expanded = ctx.expand_location("$(execpaths " + label_str + ")", targets = extra_opts_targets)
168+
expanded_paths = expanded.split(" ")
169+
170+
# Filter for the path matching the filter pattern
171+
matched_path = None
172+
for p in expanded_paths:
173+
if filter_pattern in p:
174+
matched_path = p
175+
break
176+
177+
if not matched_path:
178+
fail("filter_execpath: no path matching '{}' found in execpaths of {}. Paths: {}".format(
179+
filter_pattern,
180+
label_str,
181+
expanded,
182+
))
183+
184+
# Extract the path suffix after "/bin/" — this is the original path relative
185+
# to the output base. Since _relocate() symlinks source files under
186+
# <source_prefix>/<original_path>, the relocated file lives at
187+
# <source_dir>/<suffix_part> where source_dir is the Sphinx source directory.
188+
# Breathe resolves breathe_projects paths relative to source_dir (app.srcdir),
189+
# so we must return just the suffix_part.
190+
bin_marker = "/bin/"
191+
bin_idx = matched_path.find(bin_marker)
192+
if bin_idx >= 0:
193+
suffix_part = matched_path[bin_idx + len(bin_marker):]
194+
else:
195+
suffix_part = matched_path
196+
197+
expanded_opt = flag + "=" + suffix_part
198+
else:
199+
# Standard extra_opts: expand locations and pass through
200+
expanded_opt = ctx.expand_location(opt, targets = extra_opts_targets)
201+
202+
args.add(expanded_opt)
203+
run_args.append(expanded_opt)
204+
116205
# Collect all transitive dependencies with deduplication
117206
modules = []
118207
sphinx_toolchain = ctx.toolchains["//bazel/rules/rules_score:toolchain_type"].sphinxinfo
@@ -138,19 +227,6 @@ def _score_html_impl(ctx):
138227
content = json.encode_indent(needs_external_needs, indent = " "),
139228
)
140229

141-
# Read template and substitute PROJECT_NAME
142-
config_file = ctx.actions.declare_file(ctx.label.name + "/conf.py")
143-
template = sphinx_toolchain.conf_template.files.to_list()[0]
144-
145-
ctx.actions.expand_template(
146-
template = template,
147-
output = config_file,
148-
substitutions = {
149-
"{PROJECT_NAME}": ctx.label.name.replace("_", " ").title(),
150-
},
151-
)
152-
153-
source_prefix = ctx.label.name
154230
sphinx_source_files = []
155231

156232
# Materialize a file under the `_sources` dir
@@ -182,13 +258,6 @@ def _score_html_impl(ctx):
182258
new_path = entry.prefix + original.short_path.removeprefix(entry.strip_prefix)
183259
_relocate(original, new_path)
184260

185-
needs_external_needs_json = ctx.actions.declare_file(ctx.label.name + "/needs_external_needs.json")
186-
187-
ctx.actions.write(
188-
output = needs_external_needs_json,
189-
content = json.encode_indent(needs_external_needs, indent = " "),
190-
)
191-
192261
config_file = _create_config_py(ctx)
193262

194263
# Sphinx only accepts a single directory to read its doc sources from.
@@ -219,7 +288,7 @@ def _score_html_impl(ctx):
219288
ctx.actions.run(
220289
inputs = html_inputs,
221290
outputs = [sphinx_html_output],
222-
arguments = html_args,
291+
arguments = html_args + [args],
223292
progress_message = "Building HTML: %s" % ctx.label.name,
224293
executable = sphinx_toolchain.sphinx.files_to_run.executable,
225294
tools = [
@@ -287,6 +356,10 @@ _score_html = rule(
287356
allow_files = True,
288357
doc = "Submodule symbols.needs targets for this module.",
289358
),
359+
extra_opts = attr.string_list(
360+
doc = "Additional options to pass onto Sphinx. These are added after " +
361+
"other options, but before the source/output args.",
362+
),
290363
),
291364
toolchains = ["//bazel/rules/rules_score:toolchain_type"],
292365
)
@@ -303,6 +376,7 @@ def sphinx_module(
303376
docs_library_deps = [],
304377
sphinx = Label("//bazel/rules/rules_score:score_build"),
305378
strip_prefix = "",
379+
extra_opts = [],
306380
testonly = False,
307381
visibility = ["//visibility:public"]):
308382
"""Build a Sphinx module with transitive HTML dependencies.
@@ -323,6 +397,9 @@ def sphinx_module(
323397
source files. e.g., given `//sphinxdocs/docs:foo.md`, stripping `docs/` makes
324398
Sphinx see `foo.md` in its generated source directory. If not
325399
specified, then {any}`native.package_name` is used.
400+
extra_opts: {type}`list[str]` Additional options to pass onto Sphinx building.
401+
On each provided option, a location expansion is performed.
402+
See {any}`ctx.expand_location`.
326403
visibility: Bazel visibility
327404
"""
328405
_score_needs(
@@ -341,6 +418,7 @@ def sphinx_module(
341418
deps = deps,
342419
docs_library_deps = docs_library_deps,
343420
needs = [d + "_needs" for d in deps],
421+
extra_opts = extra_opts,
344422
testonly = testonly,
345423
visibility = visibility,
346424
)

bazel/rules/rules_score/rules_score.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ load(
4848
)
4949
load(
5050
"//bazel/rules/rules_score/private:sphinx_module.bzl",
51+
_filter_execpath = "filter_execpath",
5152
_sphinx_module = "sphinx_module",
5253
)
5354
load(
@@ -65,6 +66,7 @@ component_requirements = _component_requirements
6566
dependability_analysis = _dependability_analysis
6667
feature_requirements = _feature_requirements
6768
fmea = _fmea
69+
filter_execpath = _filter_execpath
6870
sphinx_module = _sphinx_module
6971
unit = _unit
7072
unit_design = _unit_design

bazel/rules/rules_score/src/sphinx_wrapper.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,15 @@ def validate_arguments(args: argparse.Namespace) -> None:
110110
raise ValueError(f"Index file does not exist: {args.index_file}")
111111

112112

113-
def build_sphinx_arguments(args: argparse.Namespace) -> List[str]:
113+
def build_sphinx_arguments(
114+
args: argparse.Namespace, extra_args: List[str] = None
115+
) -> List[str]:
114116
"""
115117
Build the argument list for Sphinx.
116118
117119
Args:
118120
args: Parsed command-line arguments
121+
extra_args: Additional arguments to forward to Sphinx (e.g., -D options from extra_opts)
119122
120123
Returns:
121124
List of arguments to pass to Sphinx
@@ -154,6 +157,10 @@ def build_sphinx_arguments(args: argparse.Namespace) -> List[str]:
154157

155158
base_arguments.extend(["-b", args.builder])
156159

160+
# Forward extra options (e.g., -D flags) to Sphinx
161+
if extra_args:
162+
base_arguments.extend(extra_args)
163+
157164
return base_arguments
158165

159166

@@ -240,7 +247,7 @@ def parse_arguments() -> argparse.Namespace:
240247
help=f"Port to use for live preview (default: {DEFAULT_PORT}). Use 0 for auto-detection.",
241248
)
242249

243-
return parser.parse_args()
250+
return parser.parse_known_args()
244251

245252

246253
def main() -> int:
@@ -251,14 +258,17 @@ def main() -> int:
251258
Exit code (0 for success, non-zero for failure)
252259
"""
253260
try:
254-
args = parse_arguments()
261+
args, extra_args = parse_arguments()
255262
validate_arguments(args)
263+
logger.info(f"[DEBUG] extra_args from parse_known_args: {extra_args}")
264+
logger.info(f"[DEBUG] sys.argv was: {sys.argv}")
256265
# Create processor instance
257266
stdout_processor = StdoutProcessor()
258267
stderr_processor = StderrProcessor()
259268
# Redirect stdout and stderr
260269
with redirect_stderr(stderr_processor), redirect_stdout(stdout_processor):
261-
sphinx_args = build_sphinx_arguments(args)
270+
sphinx_args = build_sphinx_arguments(args, extra_args)
271+
logger.info(f"[DEBUG] Final sphinx_args: {sphinx_args}")
262272
exit_code = run_sphinx_build(sphinx_args, args.builder)
263273
return exit_code
264274
except ValueError as e:

0 commit comments

Comments
 (0)