@@ -19,6 +19,39 @@ load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library")
1919load ("@rules_python//sphinxdocs/private:sphinx_docs_library_info.bzl" , "SphinxDocsLibraryInfo" )
2020load ("//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+
2255def _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 )
0 commit comments