Skip to content
Closed
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
257 changes: 189 additions & 68 deletions bazel/rules/rules_score/private/architectural_design.bzl

Large diffs are not rendered by default.

46 changes: 37 additions & 9 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,31 +349,40 @@ 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)

return (output_files, index_refs)

def _process_architectural_design_files(ctx, label):
def _process_architectural_design_files(ctx, label, path_prefix = ""):
"""Process all files from an architectural_design label, returning output_files and classified refs.

Args:
ctx: Rule context
label: architectural_design label to process
path_prefix: Optional subdirectory used to disambiguate multiple architectural_design labels

Returns:
Tuple of (output_files, static_refs, dynamic_refs, public_api_refs, internal_api_refs, unclassified_refs)
"""
Expand Down Expand Up @@ -414,11 +433,12 @@ def _process_architectural_design_files(ctx, label):
"architectural_design",
artifact_file,
relative_path,
path_prefix,
)
output_files.append(output_file)

if _is_document_file(artifact_file):
doc_path = "architectural_design/" + relative_path
doc_path = "architectural_design/" + path_prefix + relative_path
doc_ref = doc_path.removesuffix(".rst").removesuffix(".md")
if artifact_file.path in static_paths:
static_refs.append(doc_ref)
Expand All @@ -438,6 +458,7 @@ def _process_architectural_design_files(ctx, label):
"architectural_design",
artifact_file,
relative_path,
path_prefix,
)
output_files.append(output_file)

Expand Down Expand Up @@ -577,11 +598,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 Expand Up @@ -1003,8 +1026,13 @@ def _dependable_element_index_impl(ctx):
arch_unclassified_refs = []

if ctx.attr.architectural_design:
for ad_target in ctx.attr.architectural_design:
ad_files, s_refs, d_refs, p_refs, i_refs, u_refs = _process_architectural_design_files(ctx, ad_target)
use_label_subdirectories = len(ctx.attr.architectural_design) > 1
for index, ad_target in enumerate(ctx.attr.architectural_design):
ad_files, s_refs, d_refs, p_refs, i_refs, u_refs = _process_architectural_design_files(
ctx,
ad_target,
path_prefix = "source_{}/".format(index) if use_label_subdirectories else "",
)
output_files.extend(ad_files)
arch_static_refs.extend(s_refs)
arch_dynamic_refs.extend(d_refs)
Expand Down
100 changes: 83 additions & 17 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,36 +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 = []
pkg_prefix = ctx.label.package + "/" if ctx.label.package else ""
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 = ""

rel_dir = ""
if pkg_prefix and f.short_path.startswith(pkg_prefix):
rel_path = f.short_path[len(pkg_prefix):]
if "/" in rel_path:
rel_dir = rel_path.rsplit("/", 1)[0]
# 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(
"{}/{}".format(output_dir, wrapper_relative_path),
)

out_file_path = "{}/{}/{}{}.rst".format(output_dir, rel_dir, filename_prefix, stem) if rel_dir else "{}/{}{}.rst".format(output_dir, filename_prefix, stem)
wrapper = ctx.actions.declare_file(out_file_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 @@ -208,6 +208,7 @@ ArchitecturalDesignInfo = provider(
"dynamic_doc_files": "Depset of Sphinx doc File objects for dynamic architecture views.",
"public_api_doc_files": "Depset of Sphinx doc File objects for public API views.",
"internal_api_doc_files": "Depset of Sphinx doc File objects for internal API views.",
"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
Loading
Loading