Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions bazel/rules/rules_score/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ architectural_design(
Diagrams in `public_api` are classified separately so their lobster items flow
through `public_api_lobster_files` for failure-mode traceability.

`static_view` is an optional additional section for component diagrams that
present a partial view of the static architecture (e.g. a diagram scoped to a
subsystem). Diagrams passed to `static_view` are parsed like `static`, but are
never used to define the units/components validated against the Bazel
component graph. Instead, every component/unit defined in a `static_view`
diagram must also be defined, under the same parent, in `static`: it may only
contain a subset of the units/components of the matching `static` diagram.
**`bazel build`** fails if a `static_view` diagram introduces a
component/unit that is not present in `static`.

The `static_view` section can be used for creating additional diagrams that
provide a view onto the architecture which make the design easier to view / understand.
E.g. you can create a diagram which shows a subset of components as showing all
components in one view may be too "busy". It can also be useful when showing the
interfaces between components. Adding all the interfaces in the diagrams in the
`static` view may result in too many interface lines which is not readable. Instead,
a view can be created with a subset of components and only the interfaces between these
chosen components can be shown.

---

## `unit`
Expand Down
11 changes: 8 additions & 3 deletions bazel/rules/rules_score/docs/rule_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -390,15 +390,16 @@ Example glossary source (``.rst``):
architectural_design
~~~~~~~~~~~~~~~~~~~~

Bundles static, dynamic, public-API, and internal-API architecture views into a
single target. Provides ``ArchitecturalDesignInfo`` consumed by ``dependable_element``
and ``fmea``.
Bundles static, dynamic, static-view, public-API, and internal-API architecture
views into a single target. Provides ``ArchitecturalDesignInfo`` consumed by
``dependable_element`` and ``fmea``.

.. code-block:: python

architectural_design(
name = "arch",
static = ["docs/static_design.puml"],
static_view = ["docs/subsystem_view.puml"],
dynamic = ["docs/sequence.puml"],
public_api = ["docs/public_api.puml"],
internal_api = ["docs/internal_api.puml"],
Expand All @@ -420,6 +421,10 @@ and ``fmea``.
- label list
- no
- Static-view files (``.puml``, ``.rst``, ``.md``, ``.svg``, ``.png``) (default ``[]``)
* - ``static_view``
- label list
- no
- Component diagrams (``.puml``, ``.plantuml``) that present a partial view of the static architecture. These can be used to create smaller diagrams which highlight a subset of all components / units to improve readability / understandability. Components and units defined in a static view must also be defined under the same parent in ``static`` (default ``[]``)
* - ``dynamic``
- label list
- no
Expand Down
169 changes: 135 additions & 34 deletions bazel/rules/rules_score/private/architectural_design.bzl

Large diffs are not rendered by default.

26 changes: 21 additions & 5 deletions bazel/rules/rules_score/private/dependable_element.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ def _find_common_directory(files):
# of whether the file is a source or a generated artifact.
dirs = [paths.dirname(f.short_path) for f in files]

generated_dirs = [
paths.dirname(f.short_path)
for f in files
if not f.is_source
]
if generated_dirs:
dirs = generated_dirs

if not dirs:
return ""

Expand Down Expand Up @@ -267,20 +275,21 @@ def _is_document_file(file):
"""
return file.extension in ["rst", "md"]

def _create_artifact_symlink(ctx, artifact_name, artifact_file, relative_path):
def _create_artifact_symlink(ctx, artifact_name, artifact_file, relative_path, path_prefix = ""):
"""Create symlink for artifact file in output directory.

Args:
ctx: Rule context
artifact_name: Name of artifact type (e.g., "architectural_design")
artifact_file: Source file
relative_path: Relative path within artifact directory
path_prefix: Optional subdirectory used to disambiguate multiple providers

Returns:
Declared output file
"""
output_file = ctx.actions.declare_file(
ctx.label.name + "/" + artifact_name + "/" + relative_path,
ctx.label.name + "/" + artifact_name + "/" + path_prefix + relative_path,
)

ctx.actions.symlink(
Expand All @@ -290,13 +299,14 @@ def _create_artifact_symlink(ctx, artifact_name, artifact_file, relative_path):

return output_file

def _process_artifact_files(ctx, artifact_name, label):
def _process_artifact_files(ctx, artifact_name, label, path_prefix = ""):
"""Process all files from a single label for a given artifact type.

Args:
ctx: Rule context
artifact_name: Name of artifact type
label: Label to process
path_prefix: Optional subdirectory used to disambiguate multiple providers

Returns:
Tuple of (output_files, index_references)
Expand Down Expand Up @@ -339,23 +349,27 @@ def _process_artifact_files(ctx, artifact_name, label):
artifact_name,
artifact_file,
relative_path,
path_prefix,
)
output_files.append(output_file)

# Add to toctree index only for files directly owned by this rule.
if _is_document_file(artifact_file):
doc_path = artifact_name + "/" + relative_path
doc_path = artifact_name + "/" + path_prefix + relative_path
doc_ref = doc_path.removesuffix(".rst").removesuffix(".md")
index_refs.append(doc_ref)

# Process aux_srcs: symlink without adding to outer toctree index.
for artifact_file in aux_files:
if artifact_file.path in srcs_paths:
continue
relative_path = _compute_relative_path(artifact_file, common_dir)
output_file = _create_artifact_symlink(
ctx,
artifact_name,
artifact_file,
relative_path,
path_prefix,
)
output_files.append(output_file)

Expand All @@ -379,11 +393,13 @@ def _process_artifact_type(ctx, artifact_name):
return (output_files, index_refs)

# Process each label
for label in attr_list:
use_label_subdirectories = len(attr_list) > 1
for index, label in enumerate(attr_list):
label_outputs, label_refs = _process_artifact_files(
ctx,
artifact_name,
label,
path_prefix = "source_{}/".format(index) if use_label_subdirectories else "",
)
output_files.extend(label_outputs)
index_refs.extend(label_refs)
Expand Down
93 changes: 83 additions & 10 deletions bazel/rules/rules_score/private/puml_utils.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,29 @@

"""Shared helper for generating RST wrapper pages for PlantUML diagram files."""

def make_puml_rst_wrappers(ctx, puml_files, output_dir, template, strip_prefix = "", filename_prefix = ""):
"""Generate a thin RST wrapper page for each PlantUML diagram file.
load("@bazel_skylib//lib:paths.bzl", "paths")

def _relative_source_path(file, package):
prefix = package + "/" if package else ""
if file.short_path.startswith(prefix):
return file.short_path[len(prefix):]
return file.basename

def _directory_title(directory):
if not directory:
return "Architectural Design"
return directory.split("/")[-1].replace("_", " ").title()

def make_puml_rst_navigation(ctx, puml_files, output_dir, template, strip_prefix = "", filename_prefix = "", stems = None):
"""Generate PlantUML wrapper pages and indexes matching source directories.

The wrapper embeds the diagram via ``.. uml::`` so it appears as a
proper toctree entry while keeping the source ``.puml`` file separate.

When disambiguated stems are provided (for collision handling), the stems
are used in place of plain basenames while preserving the source directory
structure for navigation and sidebar visibility.

Args:
ctx: Rule context.
puml_files: Iterable of File objects whose extension is ``puml`` or
Expand All @@ -31,29 +48,85 @@ def make_puml_rst_wrappers(ctx, puml_files, output_dir, template, strip_prefix =
the human-readable title (e.g. ``"fta_"``).
filename_prefix: Optional prefix prepended to the output RST filename
stem (e.g. ``"detail_"``).
stems: Optional dict from File.path to a precomputed unique
stem (see architectural_design.bzl's
_disambiguated_stems), used instead of the plain
basename stem for both the output filename and the
embedded ``.. uml::`` reference -- needed when the
diagram was colocated under a disambiguated name to
avoid colliding with a same-named diagram elsewhere.

Returns:
List of declared ``.rst`` output Files, one per input diagram.
Struct containing ``wrappers``, ``indexes``, and ``root_index``.
"""
wrappers = []
diagrams_by_directory = {}
directories = {"": True}
for f in puml_files:
if f.extension not in ("puml", "plantuml"):
continue
stem = f.basename[:-(len(f.extension) + 1)]
if strip_prefix and stem.startswith(strip_prefix):
stem = stem[len(strip_prefix):]
title = stem.replace("_", " ").title()
relative_path = _relative_source_path(f, ctx.label.package)
relative_directory = paths.dirname(relative_path)
if relative_directory == ".":
relative_directory = ""

# Use disambiguated stem for generated filenames, but keep the title
# based on the source basename so the sidebar does not show a path.
source_stem = paths.basename(relative_path)[:-(len(f.extension) + 1)]
stem = stems[f.path] if stems else source_stem
title = source_stem
if strip_prefix and title.startswith(strip_prefix):
title = title[len(strip_prefix):]
title = title.replace("_", " ").title()
wrapper_relative_path = paths.join(relative_directory, filename_prefix + stem + ".rst")
wrapper = ctx.actions.declare_file(
"{}/{}{}.rst".format(output_dir, filename_prefix, stem),
"{}/{}".format(output_dir, wrapper_relative_path),
)

# For the embedded diagram filename, use disambiguated stem if available
basename = "{}.{}".format(stems[f.path], f.extension) if stems else f.basename
ctx.actions.expand_template(
template = template,
output = wrapper,
substitutions = {
"{title}": title,
"{underline}": "=" * len(title),
"{basename}": f.basename,
"{basename}": basename,
},
)
wrappers.append(wrapper)
return wrappers
diagrams_by_directory.setdefault(relative_directory, []).append(stem)
directory_parts = relative_directory.split("/") if relative_directory else []
for part_count in range(1, len(directory_parts) + 1):
directories["/".join(directory_parts[:part_count])] = True

indexes = []
for directory in sorted(directories.keys()):
entries = []
for stem in sorted(diagrams_by_directory.get(directory, [])):
entries.append(stem)
directory_prefix = directory + "/" if directory else ""
for child in sorted(directories.keys()):
child_prefix = directory_prefix
if child.startswith(child_prefix) and child != directory:
remainder = child[len(child_prefix):]
if "/" not in remainder:
entries.append(remainder + "/index")
index = ctx.actions.declare_file(
"{}/{}".format(output_dir, paths.join(directory, "index.rst")),
)
ctx.actions.write(
output = index,
content = "{}\n{}\n\n.. toctree::\n :maxdepth: 1\n\n{}\n".format(
_directory_title(directory),
"-" * len(_directory_title(directory)),
"\n".join([" " + entry for entry in entries]),
),
)
indexes.append(index)

return struct(
wrappers = wrappers,
indexes = indexes,
root_index = indexes[0] if indexes else None,
)
1 change: 1 addition & 0 deletions bazel/rules/rules_score/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ ArchitecturalDesignInfo = provider(
"dynamic": "Depset of FlatBuffers binaries for dynamic architecture diagrams (sequence diagrams, activity diagrams, etc.)",
"public_api": "Depset of FlatBuffers binaries for public API diagrams (class diagrams, etc.)",
"internal_api": "Depset of FlatBuffers binaries for internal API diagrams (class diagrams, etc.)",
"static_view": "Depset of FlatBuffers binaries for static_view component diagrams (partial views of the static architecture, validated for consistency against static).",
"name": "Name of the architectural design target",
"public_api_lobster_files": "Depset of .lobster traceability files generated from public_api diagrams.",
"validation_logs": "List of validation log entries produced by this architectural design target. Each entry has file and name fields.",
Expand Down
46 changes: 46 additions & 0 deletions bazel/rules/rules_score/src/sphinx_module_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@
"""

from pathlib import Path
import re
from typing import Any, Dict

from bazel_sphinx_needs import load_external_needs
from sphinx_conf_helpers import init_hermetic_tools


_DIRECTORY_INDEX_ANCHOR = re.compile(
r'<a(?P<before>[^>]*?)href="[^"]+/index\.html"(?P<after>[^>]*)>(?P<label>.*?)</a>',
re.DOTALL,
)


def init_external_needs(app: Any, config: Any) -> None:
"""
Initialize external needs configuration.
Expand All @@ -46,6 +53,44 @@ def init_external_needs(app: Any, config: Any) -> None:
config.needs_external_needs = load_external_needs(Path(app.confdir))


def render_directory_labels_without_links(
app: Any,
pagename: str,
templatename: str,
context: Dict[str, Any],
doctree: Any,
) -> None:
"""Remove navigation links for generated directory index pages.

Directory indexes exist only to provide expandable navigation groups. The
sidebar should expose their names as labels, while diagram pages remain
normal links.
"""

def wrap_toctree_renderer(renderer: Any) -> Any:
def render_without_directory_links(*args: Any, **kwargs: Any) -> str:
kind = args[0] if args else kwargs.get("kind")
if kind == "sidebar":
kwargs["show_nav_level"] = 100
kwargs["maxdepth"] = 100
html = renderer(*args, **kwargs)
return _DIRECTORY_INDEX_ANCHOR.sub(
lambda match: "<span{}{}>{}</span>".format(
match.group("before"),
match.group("after"),
match.group("label"),
),
str(html),
)

return render_without_directory_links

for renderer_name in ("toctree", "generate_toctree_html"):
renderer = context.get(renderer_name)
if renderer is not None:
context[renderer_name] = wrap_toctree_renderer(renderer)


def setup(app: Any) -> Dict[str, Any]:
"""
Sphinx setup hook to register event listeners.
Expand All @@ -58,6 +103,7 @@ def setup(app: Any) -> Dict[str, Any]:
"""
app.connect("config-inited", init_external_needs)
app.connect("config-inited", init_hermetic_tools)
app.connect("html-page-context", render_directory_labels_without_links, priority=900)

return {
"version": "1.0",
Expand Down
Loading
Loading