Skip to content

Commit c019fa2

Browse files
hoe-jocastler
authored andcommitted
[rules score]: fix sphinx build
- CSS Theme for pydata now correctly included (was ignored earlier) - Fix Source Links in Lobster RST Report - Some Layout improvements
1 parent 5505548 commit c019fa2

7 files changed

Lines changed: 147 additions & 50 deletions

File tree

bazel/rules/rules_score/private/dependable_element.bzl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,9 +1026,12 @@ def _dependable_element_index_impl(ctx):
10261026
lobster_rst_dir = ctx.actions.declare_directory(
10271027
ctx.label.name + "/traceability_report",
10281028
)
1029-
package = ctx.label.package
1030-
package_depth = len(package.split("/")) if package else 0
1031-
source_root = "/".join([".." for _ in range(package_depth + 2)]) + "/"
1029+
1030+
source_root = ctx.var.get("LOBSTER_SOURCE_ROOT", "")
1031+
if not source_root:
1032+
package = ctx.label.package
1033+
package_depth = len(package.split("/")) if package else 0
1034+
source_root = "/".join([".." for _ in range(package_depth + 2)]) + "/"
10321035
rst_args = ctx.actions.args()
10331036
rst_args.add(lobster_report_file.path)
10341037
rst_args.add_all(["--out-dir", lobster_rst_dir.path])

bazel/rules/rules_score/private/sphinx_module.bzl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,18 @@ def _score_html_impl(ctx):
256256
merge_inputs.append(dep_html_dir)
257257
merge_args.extend(["--dep", dep_name + ":" + dep_html_dir.path])
258258

259+
# Auto-detect static files from srcs: any file whose short_path contains
260+
# '/_static/' is a static asset that Sphinx may not copy correctly in the
261+
# Bazel sandbox (confdir != srcdir prevents html_static_path from resolving).
262+
# Copy them explicitly into output/_static/ via the merge step.
263+
for orig_file in ctx.files.srcs:
264+
path = orig_file.short_path
265+
static_marker = "/_static/"
266+
if static_marker in path:
267+
subpath = path[path.index(static_marker) + len(static_marker):]
268+
merge_args.extend(["--extra-static", orig_file.path + ":" + subpath])
269+
merge_inputs.append(orig_file)
270+
259271
# Merging html files
260272
ctx.actions.run(
261273
inputs = merge_inputs,

bazel/rules/rules_score/src/sphinx_html_merge.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,20 +138,30 @@ def copy_tree(src, dst, rel_path):
138138
copy_tree(src_path, dst_path, Path("."))
139139

140140

141-
def merge_html_dirs(output_dir, main_html_dir, dependencies):
141+
def merge_html_dirs(output_dir, main_html_dir, dependencies, extra_static=None):
142142
"""Merge HTML directories.
143143
144144
Args:
145145
output_dir: Target output directory
146146
main_html_dir: Main module's HTML directory to copy as-is
147147
dependencies: List of (name, path) tuples for dependency modules
148+
extra_static: List of (src_file, dest_subpath) tuples for extra files to
149+
place in output/_static/. These are copied AFTER the main
150+
HTML so they overwrite any theme-provided files if needed.
148151
"""
149152
output_path = Path(output_dir)
150153

151154
# First, copy the main HTML directory
152155
logging.info("Copying main HTML from %s to %s", main_html_dir, output_dir)
153156
copy_html_files(main_html_dir, output_dir)
154157

158+
# Copy any extra static files into output/_static/
159+
for src_file, dest_subpath in extra_static or []:
160+
dst = output_path / "_static" / dest_subpath
161+
dst.parent.mkdir(parents=True, exist_ok=True)
162+
shutil.copy2(src_file, dst)
163+
logging.info("Copied extra static %s → _static/%s", src_file, dest_subpath)
164+
155165
# Collect all dependency names for link fixing and exclusion
156166
dep_names = [name for name, _ in dependencies]
157167

@@ -187,6 +197,13 @@ def main():
187197
metavar="NAME:PATH",
188198
help="Dependency HTML directory in format NAME:PATH",
189199
)
200+
parser.add_argument(
201+
"--extra-static",
202+
action="append",
203+
default=[],
204+
metavar="SRC:SUBPATH",
205+
help="Extra file to place in output/_static/. Format: SRC_FILE:DEST_SUBPATH",
206+
)
190207
parser.add_argument(
191208
"--log-level",
192209
choices=["error", "warn", "info", "debug"],
@@ -212,8 +229,19 @@ def main():
212229
name, path = dep_spec.split(":", 1)
213230
dependencies.append((name, path))
214231

232+
# Parse extra static files
233+
extra_static = []
234+
for spec in args.extra_static:
235+
if ":" not in spec:
236+
logging.error(
237+
"Invalid --extra-static format '%s', expected SRC:SUBPATH", spec
238+
)
239+
return 1
240+
src, subpath = spec.split(":", 1)
241+
extra_static.append((src, subpath))
242+
215243
# Merge the HTML directories
216-
merge_html_dirs(args.output, args.main, dependencies)
244+
merge_html_dirs(args.output, args.main, dependencies, extra_static=extra_static)
217245

218246
logging.info("Successfully merged HTML into %s", args.output)
219247
return 0

bazel/rules/rules_score/templates/conf.template.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@
153153
plantuml = f"{plantuml_path} -Playout=smetana"
154154
plantuml_output_format = "svg_obj"
155155

156+
import shutil as _shutil
157+
158+
graphviz_dot = os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot"
159+
156160
# HTML theme
157161
html_theme = "sphinx_rtd_theme"
158162

tools/lobster_rst_report/_helpers.py

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
from typing import Dict, Tuple
2626

27+
import os
28+
2729
from lobster.common.report import Report
2830
from lobster.common.location import (
2931
Void_Reference,
@@ -32,6 +34,7 @@
3234
Codebeamer_Reference,
3335
)
3436
from lobster.common.items import Item, Requirement, Implementation, Activity
37+
from .graphviz_utils import is_dot_available
3538

3639

3740
class RstUtils:
@@ -176,6 +179,8 @@ def location_link(location, source_root: str = "") -> str:
176179
# lobster-trace: rst_req.RST_Source_Root_Prefix
177180
if isinstance(location, File_Reference):
178181
href = source_root + location.filename if source_root else location.filename
182+
if location.line:
183+
href += f"#L{location.line}"
179184
return f"`{e(location.to_string())} <{href}>`__"
180185

181186
# lobster-trace: UseCases.Item_GitHub_Source
@@ -339,9 +344,38 @@ def dot_escape(text: str) -> str:
339344
"""
340345
return text.replace("\\", "\\\\").replace('"', '\\"')
341346

347+
@classmethod
348+
def _build_dot_lines(cls, report: Report) -> list:
349+
"""Return the raw DOT diagram lines (without the RST directive wrapper)."""
350+
lines = []
351+
lines.append("digraph tracing_policy {")
352+
lines.append(" rankdir=TB;")
353+
lines.append(
354+
' node [shape=box, style=filled, fontname="Helvetica", margin="0.3,0.1"];'
355+
)
356+
lines.append(" edge [arrowhead=open];")
357+
lines.append("")
358+
for level_name, level in report.config.items():
359+
fill, font = cls.KIND_COLORS.get(level.kind, ("#9E9E9E", "white"))
360+
safe = cls.dot_escape(level_name)
361+
lines.append(f' "{safe}" [fillcolor="{fill}", fontcolor="{font}"];')
362+
for level_name, level in report.config.items():
363+
for trace_target in level.traces:
364+
src = cls.dot_escape(level_name)
365+
dst = cls.dot_escape(trace_target)
366+
lines.append(f' "{src}" -> "{dst}";')
367+
lines.append("}")
368+
return lines
369+
342370
@classmethod
343371
def build(cls, report: Report, indent: int = 0) -> list:
344-
"""Return RST lines for a ``.. graphviz::`` tracing-policy diagram.
372+
"""Return RST lines for the tracing-policy diagram.
373+
374+
When Graphviz ``dot`` is available, emits a ``.. graphviz::`` directive
375+
that Sphinx renders as an image. When ``dot`` is not found, emits a
376+
``.. note::`` explaining how to install Graphviz together with the raw
377+
DOT source in a ``.. code-block::`` so the diagram can still be
378+
visualised manually.
345379
346380
Args:
347381
report: The loaded LOBSTER report whose ``config`` provides level
@@ -356,28 +390,34 @@ def build(cls, report: Report, indent: int = 0) -> list:
356390
# lobster-trace: rst_req.RST_Report_Tracing_Policy_Diagram
357391
indent_str = " " * indent
358392
nested_indent = indent_str + " "
393+
dot_lines = cls._build_dot_lines(report)
394+
395+
# Respect an explicit GRAPHVIZ_DOT env var set by the build system
396+
# (e.g. via --action_env=GRAPHVIZ_DOT=/usr/bin/dot in CI).
397+
dot_bin = os.environ.get("GRAPHVIZ_DOT") or None
398+
if is_dot_available(dot_bin):
399+
out = []
400+
out.append(f"{indent_str}.. graphviz::")
401+
out.append("")
402+
for dot_line in dot_lines:
403+
out.append(f"{nested_indent}{dot_line}")
404+
out.append("")
405+
return out
359406
out = []
360-
out.append(f"{indent_str}.. graphviz::")
407+
out.append(f"{indent_str}.. note::")
361408
out.append("")
362-
out.append(f"{nested_indent}digraph tracing_policy {{")
363-
out.append(f"{nested_indent} rankdir=TB;")
364409
out.append(
365-
f"{nested_indent} node [shape=box, style=filled, "
366-
f'fontname="Helvetica", margin="0.3,0.1"];'
410+
f"{nested_indent}The tracing-policy diagram below could not be rendered "
411+
f"because the Graphviz ``dot`` utility was not found."
412+
)
413+
out.append(
414+
f"{nested_indent}Install `Graphviz <https://graphviz.org>`__ and rebuild "
415+
f"to see the diagram as an image."
367416
)
368-
out.append(f"{nested_indent} edge [arrowhead=open];")
369417
out.append("")
370-
for level_name, level in report.config.items():
371-
fill, font = cls.KIND_COLORS.get(level.kind, ("#9E9E9E", "white"))
372-
safe = cls.dot_escape(level_name)
373-
out.append(
374-
f'{nested_indent} "{safe}" [fillcolor="{fill}", fontcolor="{font}"];'
375-
)
376-
for level_name, level in report.config.items():
377-
for trace_target in level.traces:
378-
src = cls.dot_escape(level_name)
379-
dst = cls.dot_escape(trace_target)
380-
out.append(f'{nested_indent} "{src}" -> "{dst}";')
381-
out.append(f"{nested_indent}}}")
418+
out.append(f"{nested_indent}.. code-block:: dot")
419+
out.append("")
420+
for dot_line in dot_lines:
421+
out.append(f"{nested_indent} {dot_line}")
382422
out.append("")
383423
return out

tools/lobster_rst_report/_renderers.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -328,14 +328,11 @@ def build(self) -> list:
328328

329329

330330
class CoverageGridBuilder:
331-
"""Build the coverage summary section (table + policy diagram side by side).
331+
"""Build the coverage summary table.
332332
333-
Uses a sphinx-design ``.. grid:: 1 1 2 2`` layout:
334-
335-
* Left column (7/12 width on desktop) -- a ``.. list-table::`` with
336-
per-level coverage statistics and links to each level.
337-
* Right column (5/12 width on desktop) -- the :class:`PolicyDiagramBuilder`
338-
graphviz diagram showing level kinds and tracing relationships.
333+
Emits a plain ``.. list-table::`` with per-level coverage statistics and
334+
links to each level. The tracing-policy diagram is emitted separately
335+
(above this table) by the caller via :class:`PolicyDiagramBuilder`.
339336
340337
Usage::
341338
@@ -352,7 +349,7 @@ def __init__(self, report: Report):
352349
self._report = report
353350

354351
def build(self, ref_fn) -> list:
355-
"""Return RST lines for the coverage grid.
352+
"""Return RST lines for the coverage table.
356353
357354
Args:
358355
ref_fn: A callable ``(level_name: str) -> str`` returning an RST
@@ -365,29 +362,25 @@ def build(self, ref_fn) -> list:
365362
# lobster-trace: UseCases.Item_Coverage
366363
# lobster-trace: rst_req.RST_Report_Coverage_Table
367364
lines = []
368-
lines += [".. grid:: 1 1 2 2", " :gutter: 3", ""]
369-
lines += [" .. grid-item::", " :columns: 12 12 7 7", ""]
370365
lines += [
371-
" .. list-table::",
372-
" :header-rows: 1",
373-
" :widths: 35 15 15 15",
366+
".. list-table::",
367+
" :header-rows: 1",
368+
" :widths: 35 15 15 15",
374369
"",
375370
]
376371
lines += [
377-
" * - Category",
378-
" - Coverage",
379-
" - OK Items",
380-
" - Total Items",
372+
" * - Category",
373+
" - Coverage",
374+
" - OK Items",
375+
" - Total Items",
381376
]
382377
for level_name in self._report.config:
383378
data = self._report.coverage[level_name]
384-
lines.append(f" * - {ref_fn(level_name)}")
385-
lines.append(f" - {data.coverage:.1f}%")
386-
lines.append(f" - {data.ok}")
387-
lines.append(f" - {data.items}")
379+
lines.append(f" * - {ref_fn(level_name)}")
380+
lines.append(f" - {data.coverage:.1f}%")
381+
lines.append(f" - {data.ok}")
382+
lines.append(f" - {data.items}")
388383
lines.append("")
389-
lines += [" .. grid-item::", " :columns: 12 12 5 5", ""]
390-
lines += PolicyDiagramBuilder.build(self._report, indent=6)
391384
return lines
392385

393386

tools/lobster_rst_report/rst_report.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from lobster.common.errors import LOBSTER_Error
4343
from .graphviz_utils import is_dot_available
4444

45-
from ._helpers import RstUtils, ItemNaming
45+
from ._helpers import RstUtils, ItemNaming, PolicyDiagramBuilder
4646
from ._renderers import (
4747
_KIND_ORDER,
4848
_build_page_map,
@@ -89,7 +89,12 @@ def write_rst(report: Report, source_root: str = "") -> str:
8989
lines.append(f"| LOBSTER Version: {LOBSTER_VERSION}")
9090
lines.append("")
9191

92-
# Coverage table + tracing-policy diagram (rubric = not a TOC entry)
92+
# Tracing-policy diagram (rubric = not a TOC entry)
93+
lines.append(".. rubric:: Tracing Policy")
94+
lines.append("")
95+
lines += PolicyDiagramBuilder.build(report)
96+
97+
# Coverage table (rubric = not a TOC entry)
9398
lines.append(".. rubric:: Coverage Summary")
9499
lines.append("")
95100

@@ -222,7 +227,12 @@ def write_rst_pages(report: Report, source_root: str = "") -> Dict[str, str]:
222227
lines.append(f"| LOBSTER Version: {LOBSTER_VERSION}")
223228
lines.append("")
224229

225-
# Coverage table + policy diagram (rubric = not a TOC entry)
230+
# Tracing-policy diagram (rubric = not a TOC entry)
231+
lines.append(".. rubric:: Tracing Policy")
232+
lines.append("")
233+
lines += PolicyDiagramBuilder.build(report)
234+
235+
# Coverage table (rubric = not a TOC entry)
226236
lines.append(".. rubric:: Coverage Summary")
227237
lines.append("")
228238

@@ -239,14 +249,21 @@ def ref_fn(n):
239249

240250
# Per-kind toctrees -- :caption: shows in sidebar but doesn't create a
241251
# heading node, so clicking a level goes straight to that level's page
252+
# The first toctree includes 'self' so the overview page appears as a
253+
# navigation item alongside the per-level sub-pages.
254+
first = True
242255
for kind, kind_title in _KIND_ORDER:
243256
levels_of_kind = [lv for lv in report.config.values() if lv.kind == kind]
244257
if not levels_of_kind:
245258
continue
246259
lines.append(".. toctree::")
247260
lines.append(f" :caption: {kind_title}")
248261
lines.append(" :maxdepth: 1")
262+
lines.append(" :hidden:")
249263
lines.append("")
264+
if first:
265+
lines.append(" Overview <self>")
266+
first = False
250267
for lv in levels_of_kind:
251268
lines.append(f" {page_map[lv.name]}")
252269
lines.append("")

0 commit comments

Comments
 (0)