diff --git a/bazel/rules/rules_score/private/architectural_design.bzl b/bazel/rules/rules_score/private/architectural_design.bzl index 38a9c197..6e95e686 100644 --- a/bazel/rules/rules_score/private/architectural_design.bzl +++ b/bazel/rules/rules_score/private/architectural_design.bzl @@ -140,13 +140,20 @@ def _colocate_puml_with_wrapper(ctx, puml_files, output_dir): same-directory symlinked copies. """ colocated = [] + pkg_prefix = ctx.label.package + "/" if ctx.label.package else "" for f in puml_files: if f.extension not in ("puml", "plantuml"): colocated.append(f) continue - copy = ctx.actions.declare_file( - "{}/{}".format(output_dir, f.basename), - ) + + 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] + + out_path = "{}/{}/{}".format(output_dir, rel_dir, f.basename) if rel_dir else "{}/{}".format(output_dir, f.basename) + copy = ctx.actions.declare_file(out_path) ctx.actions.symlink(output = copy, target_file = f) colocated.append(copy) return colocated @@ -240,12 +247,41 @@ def _architectural_design_impl(ctx): # Generate a thin RST wrapper for every .puml diagram so it appears as a # toctree entry in the dependable_element index. - rst_wrappers = make_puml_rst_wrappers( + static_wrappers = make_puml_rst_wrappers( + ctx, + ctx.files.static, + ctx.label.name, + ctx.file._puml_rst_template, + ) + dynamic_wrappers = make_puml_rst_wrappers( + ctx, + ctx.files.dynamic, + ctx.label.name, + ctx.file._puml_rst_template, + ) + public_api_wrappers = make_puml_rst_wrappers( ctx, - ctx.files.static + ctx.files.dynamic + ctx.files.public_api + ctx.files.internal_api, + ctx.files.public_api, ctx.label.name, ctx.file._puml_rst_template, ) + internal_api_wrappers = make_puml_rst_wrappers( + ctx, + ctx.files.internal_api, + ctx.label.name, + ctx.file._puml_rst_template, + ) + + rst_wrappers = static_wrappers + dynamic_wrappers + public_api_wrappers + internal_api_wrappers + + def _get_doc_files(files, wrappers): + docs = [f for f in files if f.extension in ("rst", "md")] + return depset(docs + wrappers) + + static_doc_files = _get_doc_files(ctx.files.static, static_wrappers) + dynamic_doc_files = _get_doc_files(ctx.files.dynamic, dynamic_wrappers) + public_api_doc_files = _get_doc_files(ctx.files.public_api, public_api_wrappers) + internal_api_doc_files = _get_doc_files(ctx.files.internal_api, internal_api_wrappers) validation_log = _run_validation( ctx, @@ -264,6 +300,10 @@ def _architectural_design_impl(ctx): dynamic = dynamic_fbs, public_api = public_api_fbs, internal_api = internal_api_fbs, + static_doc_files = static_doc_files, + dynamic_doc_files = dynamic_doc_files, + public_api_doc_files = public_api_doc_files, + internal_api_doc_files = internal_api_doc_files, name = ctx.label.name, public_api_lobster_files = public_api_lobster, validation_logs = [validation_log], diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 4b5a8070..aee6960c 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -361,6 +361,204 @@ def _process_artifact_files(ctx, artifact_name, label): return (output_files, index_refs) +def _process_architectural_design_files(ctx, label): + """Process all files from an architectural_design label, returning output_files and classified refs. + + Returns: + Tuple of (output_files, static_refs, dynamic_refs, public_api_refs, internal_api_refs, unclassified_refs) + """ + output_files = [] + static_refs = [] + dynamic_refs = [] + public_api_refs = [] + internal_api_refs = [] + unclassified_refs = [] + + all_files = _get_sphinx_files(label) + doc_files = _filter_doc_files(all_files) + + aux_files = [] + if label[SphinxSourcesInfo].aux_srcs: + aux_files = label[SphinxSourcesInfo].aux_srcs.to_list() + + if not doc_files and not aux_files: + return (output_files, static_refs, dynamic_refs, public_api_refs, internal_api_refs, unclassified_refs) + + srcs_paths = {f.path: True for f in label[SphinxSourcesInfo].srcs.to_list()} + common_dir = _find_common_directory(doc_files + aux_files) + + static_paths = {} + dynamic_paths = {} + public_api_paths = {} + internal_api_paths = {} + + if ArchitecturalDesignInfo in label: + info = label[ArchitecturalDesignInfo] + if hasattr(info, "static_doc_files") and info.static_doc_files: + static_paths = {f.path: True for f in info.static_doc_files.to_list()} + if hasattr(info, "dynamic_doc_files") and info.dynamic_doc_files: + dynamic_paths = {f.path: True for f in info.dynamic_doc_files.to_list()} + if hasattr(info, "public_api_doc_files") and info.public_api_doc_files: + public_api_paths = {f.path: True for f in info.public_api_doc_files.to_list()} + if hasattr(info, "internal_api_doc_files") and info.internal_api_doc_files: + internal_api_paths = {f.path: True for f in info.internal_api_doc_files.to_list()} + + for artifact_file in doc_files: + relative_path = _compute_relative_path(artifact_file, common_dir) + + if _is_document_file(artifact_file) and artifact_file.path not in srcs_paths: + continue + + output_file = _create_artifact_symlink( + ctx, + "architectural_design", + artifact_file, + relative_path, + ) + output_files.append(output_file) + + if _is_document_file(artifact_file): + doc_path = "architectural_design/" + relative_path + doc_ref = doc_path.removesuffix(".rst").removesuffix(".md") + if artifact_file.path in static_paths: + static_refs.append(doc_ref) + elif artifact_file.path in dynamic_paths: + dynamic_refs.append(doc_ref) + elif artifact_file.path in public_api_paths: + public_api_refs.append(doc_ref) + elif artifact_file.path in internal_api_paths: + internal_api_refs.append(doc_ref) + else: + unclassified_refs.append(doc_ref) + + for artifact_file in aux_files: + relative_path = _compute_relative_path(artifact_file, common_dir) + output_file = _create_artifact_symlink( + ctx, + "architectural_design", + artifact_file, + relative_path, + ) + output_files.append(output_file) + + return (output_files, static_refs, dynamic_refs, public_api_refs, internal_api_refs, unclassified_refs) + +def _generate_software_arch_page( + ctx, + feature_req_refs, + static_refs, + dynamic_refs, + public_api_refs, + internal_api_refs, + unclassified_refs, + dependability_refs, + output_files): + """Generate software_arch.rst page with section subheadings when categorized entries exist.""" + has_categories = bool( + feature_req_refs or static_refs or dynamic_refs or + public_api_refs or internal_api_refs, + ) + + if not has_categories and not unclassified_refs and not dependability_refs: + return None + + title = "Software Architectural Level" + underline = "-" * len(title) + lines = [title, underline, ""] + + if has_categories: + if feature_req_refs: + lines.extend([ + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(feature_req_refs), + "", + ]) + if static_refs: + lines.extend([ + "Static Design", + "~~~~~~~~~~~~~", + "", + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(static_refs), + "", + ]) + if dynamic_refs: + lines.extend([ + "Dynamic Design", + "~~~~~~~~~~~~~~", + "", + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(dynamic_refs), + "", + ]) + if public_api_refs: + lines.extend([ + "Public API", + "~~~~~~~~~~", + "", + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(public_api_refs), + "", + ]) + if internal_api_refs: + lines.extend([ + "Internal API", + "~~~~~~~~~~~~", + "", + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(internal_api_refs), + "", + ]) + if unclassified_refs: + lines.extend([ + "Other Architectural Design", + "~~~~~~~~~~~~~~~~~~~~~~~~~~", + "", + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(unclassified_refs), + "", + ]) + if dependability_refs: + lines.extend([ + "Dependability Analysis", + "~~~~~~~~~~~~~~~~~~~~~~", + "", + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(dependability_refs), + "", + ]) + else: + all_entries = unclassified_refs + dependability_refs + lines.extend([ + ".. toctree::", + " :maxdepth: 1", + "", + " " + "\n ".join(all_entries), + "", + ]) + + page = ctx.actions.declare_file(ctx.label.name + "/software_arch.rst") + ctx.actions.write( + output = page, + content = "\n".join(lines), + ) + output_files.append(page) + return "software_arch" + def _process_artifact_type(ctx, artifact_name): """Process all labels for a given artifact type. @@ -787,7 +985,6 @@ def _dependable_element_index_impl(ctx): # toctree references for the index template. artifact_types = [ "assumptions_of_use", - "architectural_design", "dependability_analysis", "checklists", "glossary", @@ -799,6 +996,22 @@ def _dependable_element_index_impl(ctx): output_files.extend(files) artifacts_by_type[artifact_name] = refs + arch_static_refs = [] + arch_dynamic_refs = [] + arch_public_api_refs = [] + arch_internal_api_refs = [] + 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) + output_files.extend(ad_files) + arch_static_refs.extend(s_refs) + arch_dynamic_refs.extend(d_refs) + arch_public_api_refs.extend(p_refs) + arch_internal_api_refs.extend(i_refs) + arch_unclassified_refs.extend(u_refs) + # Collect feature_requirements refs from requirements targets that # carry FeatureRequirementsInfo. feature_req_refs = [] @@ -900,10 +1113,16 @@ def _dependable_element_index_impl(ctx): "Assumed System", assumed_system_req_refs + artifacts_by_type["assumptions_of_use"], ) - software_arch_ref = _section_page( - "software_arch", - "Software Architectural Level", - feature_req_refs + artifacts_by_type["architectural_design"] + artifacts_by_type["dependability_analysis"], + software_arch_ref = _generate_software_arch_page( + ctx, + feature_req_refs = feature_req_refs, + static_refs = arch_static_refs, + dynamic_refs = arch_dynamic_refs, + public_api_refs = arch_public_api_refs, + internal_api_refs = arch_internal_api_refs, + unclassified_refs = arch_unclassified_refs, + dependability_refs = artifacts_by_type["dependability_analysis"], + output_files = output_files, ) components_ref = _section_page( "components", diff --git a/bazel/rules/rules_score/private/puml_utils.bzl b/bazel/rules/rules_score/private/puml_utils.bzl index 0a271e4a..5673f5ec 100644 --- a/bazel/rules/rules_score/private/puml_utils.bzl +++ b/bazel/rules/rules_score/private/puml_utils.bzl @@ -36,6 +36,7 @@ def make_puml_rst_wrappers(ctx, puml_files, output_dir, template, strip_prefix = List of declared ``.rst`` output Files, one per input diagram. """ wrappers = [] + pkg_prefix = ctx.label.package + "/" if ctx.label.package else "" for f in puml_files: if f.extension not in ("puml", "plantuml"): continue @@ -43,9 +44,15 @@ def make_puml_rst_wrappers(ctx, puml_files, output_dir, template, strip_prefix = if strip_prefix and stem.startswith(strip_prefix): stem = stem[len(strip_prefix):] title = stem.replace("_", " ").title() - wrapper = ctx.actions.declare_file( - "{}/{}{}.rst".format(output_dir, filename_prefix, stem), - ) + + 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] + + 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) ctx.actions.expand_template( template = template, output = wrapper, diff --git a/bazel/rules/rules_score/providers.bzl b/bazel/rules/rules_score/providers.bzl index 908a725b..94fa80ab 100644 --- a/bazel/rules/rules_score/providers.bzl +++ b/bazel/rules/rules_score/providers.bzl @@ -204,6 +204,10 @@ 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_doc_files": "Depset of Sphinx doc File objects for static architecture views.", + "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.", "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.",