diff --git a/.bazelrc b/.bazelrc index 04d52d33..4a34f05f 100644 --- a/.bazelrc +++ b/.bazelrc @@ -16,5 +16,12 @@ build --tool_java_runtime_version=remotejdk_17 build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect build:clippy --output_groups=+clippy_checks +# Log level configuration for rules_score build output +# normal build: only errors and warnings (default – no flag needed) +# info build: additionally show info messages from all tools +build:info --//bazel/rules/rules_score:verbosity=info +# debug build: complete output including debug/trace from all tools +build:debug --//bazel/rules/rules_score:verbosity=debug + # Import AI checker custom configuration try-import %workspace%/.bazelrc.ai_checker diff --git a/bazel/rules/rules_score/BUILD b/bazel/rules/rules_score/BUILD index 2e21bb27..3eea67be 100644 --- a/bazel/rules/rules_score/BUILD +++ b/bazel/rules/rules_score/BUILD @@ -11,6 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@pip_rules_score//:requirements.bzl", "requirement") load("@rules_python//python:pip.bzl", "compile_pip_requirements") load( @@ -181,3 +182,18 @@ lobster_linker( ], visibility = ["//docs/processes:__pkg__"], ) + +# --------------------------------------------------------------------------- +# Build setting: verbosity level for all rules_score tools +# --------------------------------------------------------------------------- + +string_flag( + name = "verbosity", + build_setting_default = "warn", + values = [ + "warn", + "info", + "debug", + ], + visibility = ["//visibility:public"], +) diff --git a/bazel/rules/rules_score/private/architectural_design.bzl b/bazel/rules/rules_score/private/architectural_design.bzl index 1bcc02c4..f2b85bde 100644 --- a/bazel/rules/rules_score/private/architectural_design.bzl +++ b/bazel/rules/rules_score/private/architectural_design.bzl @@ -23,6 +23,7 @@ to produce FlatBuffers binary representations of the parsed diagrams. """ load("//bazel/rules/rules_score:providers.bzl", "ArchitecturalDesignInfo", "SphinxSourcesInfo") +load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") # ============================================================================ # Private Rule Implementation @@ -62,6 +63,8 @@ def _run_puml_parser(ctx, puml_file): fbs_output.dirname, "--lobster-output-dir", lobster_output.dirname, + "--log-level", + get_log_level(ctx), ], progress_message = "Parsing PlantUML diagram: %s" % puml_file.short_path, ) @@ -132,7 +135,7 @@ def _architectural_design_impl(ctx): inputs = all_fbs_files, outputs = [plantuml_links_json], executable = ctx.executable._linker, - arguments = ["--fbs-files"] + [f.path for f in all_fbs_files] + ["--output", plantuml_links_json.path], + arguments = ["--fbs-files"] + [f.path for f in all_fbs_files] + ["--output", plantuml_links_json.path, "--log-level", get_log_level(ctx)], progress_message = "Generating PlantUML links JSON for %s" % ctx.label.name, ) else: @@ -170,38 +173,41 @@ _architectural_design = rule( implementation = _architectural_design_impl, doc = "Collects architectural design documents and diagrams for S-CORE process compliance. " + "Automatically parses PlantUML files to produce FlatBuffers binary representations.", - attrs = { - "static": attr.label_list( - allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], - mandatory = False, - doc = "Static architecture diagrams (class diagrams, component diagrams, etc.)", - ), - "dynamic": attr.label_list( - allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], - mandatory = False, - doc = "Dynamic architecture diagrams (sequence diagrams, activity diagrams, etc.)", - ), - "public_api": attr.label_list( - allow_files = [".puml", ".plantuml"], - mandatory = False, - doc = "Public API diagrams (parsed identically to static/dynamic). " + - "Classified separately so their lobster items are exposed via " + - "public_api_lobster_files, enabling failure-mode-to-interface " + - "traceability at the dependable element level.", - ), - "_puml_parser": attr.label( - default = Label("@score_tooling//plantuml/parser:parser"), - executable = True, - cfg = "exec", - doc = "PlantUML parser tool that generates FlatBuffers from .puml files", - ), - "_linker": attr.label( - default = Label("@score_tooling//plantuml/parser:linker"), - executable = True, - cfg = "exec", - doc = "Tool that generates plantuml_links.json from FlatBuffers diagram outputs", - ), - }, + attrs = dict( + { + "static": attr.label_list( + allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], + mandatory = False, + doc = "Static architecture diagrams (class diagrams, component diagrams, etc.)", + ), + "dynamic": attr.label_list( + allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], + mandatory = False, + doc = "Dynamic architecture diagrams (sequence diagrams, activity diagrams, etc.)", + ), + "public_api": attr.label_list( + allow_files = [".puml", ".plantuml"], + mandatory = False, + doc = "Public API diagrams (parsed identically to static/dynamic). " + + "Classified separately so their lobster items are exposed via " + + "public_api_lobster_files, enabling failure-mode-to-interface " + + "traceability at the dependable element level.", + ), + "_puml_parser": attr.label( + default = Label("@score_tooling//plantuml/parser:parser"), + executable = True, + cfg = "exec", + doc = "PlantUML parser tool that generates FlatBuffers from .puml files", + ), + "_linker": attr.label( + default = Label("@score_tooling//plantuml/parser:linker"), + executable = True, + cfg = "exec", + doc = "Tool that generates plantuml_links.json from FlatBuffers diagram outputs", + ), + }, + **VERBOSITY_ATTR + ), ) # ============================================================================ diff --git a/bazel/rules/rules_score/private/dependable_element.bzl b/bazel/rules/rules_score/private/dependable_element.bzl index 31639676..c1b68310 100644 --- a/bazel/rules/rules_score/private/dependable_element.bzl +++ b/bazel/rules/rules_score/private/dependable_element.bzl @@ -51,6 +51,7 @@ load( "format_lobster_sources", ) load("//bazel/rules/rules_score/private:sphinx_module.bzl", "sphinx_module") +load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") # ============================================================================ # Template Constants @@ -642,6 +643,7 @@ def _run_validation(ctx, arch_json, static_fbs_files): validation_args.add("--architecture-json", arch_json) validation_args.add_all("--component-fbs", static_fbs_files) validation_args.add("--output", validation_log) + validation_args.add("--log-level", get_log_level(ctx)) if ctx.attr.maturity == "development": validation_args.add("--warn-on-errors") @@ -986,76 +988,79 @@ def _dependable_element_index_impl(ctx): _dependable_element_index = rule( implementation = _dependable_element_index_impl, doc = "Generates index.rst file with references to dependable element artifacts", - attrs = { - "module_name": attr.string( - mandatory = True, - doc = "Name of the dependable element module (used as document title)", - ), - "assumptions_of_use": attr.label_list( - mandatory = True, - doc = "Assumptions of Use targets or files.", - ), - "requirements": attr.label_list( - mandatory = True, - providers = [FeatureRequirementsInfo], - doc = "Feature requirements targets (feature_requirements only).", - ), - "architectural_design": attr.label_list( - mandatory = True, - doc = "Architectural design targets or files.", - ), - "dependability_analysis": attr.label_list( - mandatory = True, - doc = "Dependability analysis targets or files.", - ), - "components": attr.label_list( - mandatory = True, - aspects = [collect_current_architecture_aspect], - doc = "Component targets (aspect is applied here and passed to subrule).", - ), - "tests": attr.label_list( - default = [], - doc = "Integration tests for the dependable element.", - ), - "checklists": attr.label_list( - default = [], - doc = "Safety checklists targets or files.", - ), - "template": attr.label( - allow_single_file = [".rst"], - mandatory = True, - doc = "Template file for generating index.rst", - ), - "deps": attr.label_list( - default = [], - doc = "Dependencies on other dependable element modules (submodules).", - ), - "processed_deps": attr.label_list( - default = [], - doc = "Dependencies on other dependable element modules (submodules).", - ), - "integrity_level": attr.string( - mandatory = True, - values = _INTEGRITY_LEVELS, - doc = "Integrity level of the dependable element. Allowed values: 'A', 'B', 'C', 'D' (D > C > B > A).", - ), - "maturity": attr.string( - default = "release", - values = ["release", "development"], - doc = "Maturity level of the dependable element. 'release' (default) treats certified scope violations as errors; 'development' emits warnings and continues.", - ), - "_validation_cli": attr.label( - default = Label("//validation/core:validation_cli"), - executable = True, - cfg = "exec", - doc = "Validation CLI tool", - ), - "_lobster_de_template": attr.label( - default = Label("//bazel/rules/rules_score/lobster/config:lobster_de_template"), - allow_single_file = True, - doc = "Lobster config template for dependable element traceability.", - ), - }, + attrs = dict( + { + "module_name": attr.string( + mandatory = True, + doc = "Name of the dependable element module (used as document title)", + ), + "assumptions_of_use": attr.label_list( + mandatory = True, + doc = "Assumptions of Use targets or files.", + ), + "requirements": attr.label_list( + mandatory = True, + providers = [FeatureRequirementsInfo], + doc = "Feature requirements targets (feature_requirements only).", + ), + "architectural_design": attr.label_list( + mandatory = True, + doc = "Architectural design targets or files.", + ), + "dependability_analysis": attr.label_list( + mandatory = True, + doc = "Dependability analysis targets or files.", + ), + "components": attr.label_list( + mandatory = True, + aspects = [collect_current_architecture_aspect], + doc = "Component targets (aspect is applied here and passed to subrule).", + ), + "tests": attr.label_list( + default = [], + doc = "Integration tests for the dependable element.", + ), + "checklists": attr.label_list( + default = [], + doc = "Safety checklists targets or files.", + ), + "template": attr.label( + allow_single_file = [".rst"], + mandatory = True, + doc = "Template file for generating index.rst", + ), + "deps": attr.label_list( + default = [], + doc = "Dependencies on other dependable element modules (submodules).", + ), + "processed_deps": attr.label_list( + default = [], + doc = "Dependencies on other dependable element modules (submodules).", + ), + "integrity_level": attr.string( + mandatory = True, + values = _INTEGRITY_LEVELS, + doc = "Integrity level of the dependable element. Allowed values: 'A', 'B', 'C', 'D' (D > C > B > A).", + ), + "maturity": attr.string( + default = "release", + values = ["release", "development"], + doc = "Maturity level of the dependable element. 'release' (default) treats certified scope violations as errors; 'development' emits warnings and continues.", + ), + "_validation_cli": attr.label( + default = Label("//validation/core:validation_cli"), + executable = True, + cfg = "exec", + doc = "Validation CLI tool", + ), + "_lobster_de_template": attr.label( + default = Label("//bazel/rules/rules_score/lobster/config:lobster_de_template"), + allow_single_file = True, + doc = "Lobster config template for dependable element traceability.", + ), + }, + **VERBOSITY_ATTR + ), subrules = [subrule_lobster_report, subrule_lobster_html_report], ) diff --git a/bazel/rules/rules_score/private/fmea.bzl b/bazel/rules/rules_score/private/fmea.bzl index 64b953fd..463e3f89 100644 --- a/bazel/rules/rules_score/private/fmea.bzl +++ b/bazel/rules/rules_score/private/fmea.bzl @@ -43,6 +43,7 @@ by the ``dependability_analysis`` rule which wraps this one. load("@trlc//:trlc.bzl", "TrlcProviderInfo") load("//bazel/rules/rules_score:providers.bzl", "AnalysisInfo", "ArchitecturalDesignInfo", "SphinxSourcesInfo") +load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") # ============================================================================ # Root-cause (FTA) processing helper @@ -84,6 +85,7 @@ def _process_root_causes(ctx): args.add("--metamodel", ctx.file._fta_metamodel) args.add("--output-dir", output_dir) args.add("--lobster", root_causes_lobster) + args.add("--log-level", get_log_level(ctx)) args.add_all(puml_inputs) ctx.actions.run( inputs = puml_inputs + [ctx.file._fta_metamodel], @@ -298,70 +300,73 @@ _fmea = rule( implementation = _fmea_impl, doc = "Renders FMEA TRLC sources to .inc files and generates lobster traceability files. " + "Build-only rule; traceability testing is owned by dependability_analysis.", - attrs = { - "failuremodes": attr.label_list( - providers = [TrlcProviderInfo], - mandatory = False, - doc = "Failure modes as trlc_requirements targets (rendered to .inc via trlc_rst).", - ), - "controlmeasures": attr.label_list( - providers = [TrlcProviderInfo], - mandatory = False, - doc = "Control measures as trlc_requirements targets (rendered to .inc via trlc_rst).", - ), - "root_causes": attr.label_list( - allow_files = [".puml", ".plantuml"], - mandatory = False, - doc = "Root cause FTA PlantUML diagram files. " + - "``fta_metamodel.puml`` is inlined automatically; " + - "lobster items are extracted to ``root_causes.lobster``.", - ), - "arch_design": attr.label( - providers = [ArchitecturalDesignInfo], - mandatory = False, - doc = "Reference to architectural_design target for traceability.", - ), - "_safety_analysis_tools": attr.label( - default = Label("//bazel/rules/rules_score:safety_analysis_tools"), - executable = True, - allow_files = True, - cfg = "exec", - doc = "safety_analysis_tools binary: preprocess and extract subcommands.", - ), - "_fta_metamodel": attr.label( - default = Label("//plantuml:fta_metamodel"), - allow_single_file = True, - doc = "fta_metamodel.puml whose content is inlined into root cause diagrams.", - ), - "_renderer": attr.label( - default = Label("@trlc//tools/trlc_rst:trlc_rst"), - executable = True, - allow_files = True, - cfg = "exec", - ), - "_lobster_trlc": attr.label( - default = Label("@lobster//:lobster-trlc"), - executable = True, - allow_files = True, - cfg = "exec", - doc = "lobster-trlc executable used to generate FM and CM lobster files.", - ), - "_fm_lobster_config": attr.label( - default = Label("//bazel/rules/rules_score/lobster/config:failuremodes_config"), - allow_single_file = True, - doc = "lobster-trlc YAML config for FailureMode records.", - ), - "_cm_lobster_config": attr.label( - default = Label("//bazel/rules/rules_score/lobster/config:controlmeasures_config"), - allow_single_file = True, - doc = "lobster-trlc YAML config for ControlMeasure records.", - ), - "_template": attr.label( - default = Label("//bazel/rules/rules_score:templates/fmea.template.rst"), - allow_single_file = True, - doc = "RST template for the FMEA page.", - ), - }, + attrs = dict( + { + "failuremodes": attr.label_list( + providers = [TrlcProviderInfo], + mandatory = False, + doc = "Failure modes as trlc_requirements targets (rendered to .inc via trlc_rst).", + ), + "controlmeasures": attr.label_list( + providers = [TrlcProviderInfo], + mandatory = False, + doc = "Control measures as trlc_requirements targets (rendered to .inc via trlc_rst).", + ), + "root_causes": attr.label_list( + allow_files = [".puml", ".plantuml"], + mandatory = False, + doc = "Root cause FTA PlantUML diagram files. " + + "``fta_metamodel.puml`` is inlined automatically; " + + "lobster items are extracted to ``root_causes.lobster``.", + ), + "arch_design": attr.label( + providers = [ArchitecturalDesignInfo], + mandatory = False, + doc = "Reference to architectural_design target for traceability.", + ), + "_safety_analysis_tools": attr.label( + default = Label("//bazel/rules/rules_score:safety_analysis_tools"), + executable = True, + allow_files = True, + cfg = "exec", + doc = "safety_analysis_tools binary: preprocess and extract subcommands.", + ), + "_fta_metamodel": attr.label( + default = Label("//plantuml:fta_metamodel"), + allow_single_file = True, + doc = "fta_metamodel.puml whose content is inlined into root cause diagrams.", + ), + "_renderer": attr.label( + default = Label("@trlc//tools/trlc_rst:trlc_rst"), + executable = True, + allow_files = True, + cfg = "exec", + ), + "_lobster_trlc": attr.label( + default = Label("@lobster//:lobster-trlc"), + executable = True, + allow_files = True, + cfg = "exec", + doc = "lobster-trlc executable used to generate FM and CM lobster files.", + ), + "_fm_lobster_config": attr.label( + default = Label("//bazel/rules/rules_score/lobster/config:failuremodes_config"), + allow_single_file = True, + doc = "lobster-trlc YAML config for FailureMode records.", + ), + "_cm_lobster_config": attr.label( + default = Label("//bazel/rules/rules_score/lobster/config:controlmeasures_config"), + allow_single_file = True, + doc = "lobster-trlc YAML config for ControlMeasure records.", + ), + "_template": attr.label( + default = Label("//bazel/rules/rules_score:templates/fmea.template.rst"), + allow_single_file = True, + doc = "RST template for the FMEA page.", + ), + }, + **VERBOSITY_ATTR + ), ) # ============================================================================ diff --git a/bazel/rules/rules_score/private/rst_to_trlc.bzl b/bazel/rules/rules_score/private/rst_to_trlc.bzl index 8948462f..196b8c3f 100644 --- a/bazel/rules/rules_score/private/rst_to_trlc.bzl +++ b/bazel/rules/rules_score/private/rst_to_trlc.bzl @@ -14,6 +14,7 @@ """Bazel rule and helper macro for converting RST requirement directives to TRLC files.""" load("@trlc//:trlc.bzl", "trlc_requirements") +load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") def rst_srcs_to_trlc(name, srcs, deps = [], ref_package = ""): """Convert any .rst entries in srcs to trlc_requirements targets. @@ -75,6 +76,8 @@ def _rst_to_trlc_impl(ctx): if ctx.attr.package: args.add("--package") args.add(ctx.attr.package) + args.add("--log-level") + args.add(get_log_level(ctx)) ctx.actions.run( executable = ctx.executable._converter, @@ -90,25 +93,28 @@ def _rst_to_trlc_impl(ctx): rst_to_trlc = rule( implementation = _rst_to_trlc_impl, doc = "Converts RST requirement directives to TRLC source files.", - attrs = { - "srcs": attr.label_list( - allow_files = [".rst"], - mandatory = True, - doc = "RST files containing supported requirement directives.", - ), - "_converter": attr.label( - default = Label("//bazel/rules/rules_score:rst_to_trlc"), - executable = True, - allow_files = True, - cfg = "exec", - ), - "ref_package": attr.string( - default = "", - doc = "TRLC package prefix used for derived_from cross-references.", - ), - "package": attr.string( - default = "", - doc = "Optional TRLC package name override; defaults to the input file stem.", - ), - }, + attrs = dict( + { + "srcs": attr.label_list( + allow_files = [".rst"], + mandatory = True, + doc = "RST files containing supported requirement directives.", + ), + "_converter": attr.label( + default = Label("//bazel/rules/rules_score:rst_to_trlc"), + executable = True, + allow_files = True, + cfg = "exec", + ), + "ref_package": attr.string( + default = "", + doc = "TRLC package prefix used for derived_from cross-references.", + ), + "package": attr.string( + default = "", + doc = "Optional TRLC package name override; defaults to the input file stem.", + ), + }, + **VERBOSITY_ATTR + ), ) diff --git a/bazel/rules/rules_score/private/sphinx_module.bzl b/bazel/rules/rules_score/private/sphinx_module.bzl index 3bf1c9f3..e92446db 100644 --- a/bazel/rules/rules_score/private/sphinx_module.bzl +++ b/bazel/rules/rules_score/private/sphinx_module.bzl @@ -17,6 +17,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@rules_python//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") load("@rules_python//sphinxdocs/private:sphinx_docs_library_info.bzl", "SphinxDocsLibraryInfo") load("//bazel/rules/rules_score:providers.bzl", "FilteredExecpathInfo", "SphinxIndexFileInfo", "SphinxModuleInfo", "SphinxNeedsInfo") +load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") def _get_index_file(ctx): """Extract the index file from the index attribute. @@ -54,20 +55,23 @@ def _create_config_py(ctx): # ====================================================================================== # Common attributes for Sphinx rules # ====================================================================================== -sphinx_rule_attrs = { - "srcs": attr.label_list( - allow_files = True, - doc = "List of source files for the Sphinx documentation.", - ), - "index": attr.label( - allow_files = [".rst"], - doc = "Index file (index.rst) for the Sphinx documentation.", - mandatory = True, - ), - "deps": attr.label_list( - doc = "List of other sphinx_module targets this module depends on for intersphinx.", - ), -} +sphinx_rule_attrs = dict( + { + "srcs": attr.label_list( + allow_files = True, + doc = "List of source files for the Sphinx documentation.", + ), + "index": attr.label( + allow_files = [".rst"], + doc = "Index file (index.rst) for the Sphinx documentation.", + mandatory = True, + ), + "deps": attr.label_list( + doc = "List of other sphinx_module targets this module depends on for intersphinx.", + ), + }, + **VERBOSITY_ATTR +) # ====================================================================================== # Rule implementations @@ -91,6 +95,8 @@ def _score_needs_impl(ctx): config_file.path, "--builder", "needs", + "--log-level", + get_log_level(ctx), ] ctx.actions.run( inputs = needs_inputs, @@ -214,6 +220,8 @@ def _score_html_impl(ctx): config_file.path, "--builder", "html", + "--log-level", + get_log_level(ctx), ] ctx.actions.run( inputs = html_inputs, @@ -235,6 +243,8 @@ def _score_html_impl(ctx): html_output.path, "--main", sphinx_html_output.path, + "--log-level", + get_log_level(ctx), ] merge_inputs = [sphinx_html_output] diff --git a/bazel/rules/rules_score/private/unit_design.bzl b/bazel/rules/rules_score/private/unit_design.bzl index 125fcb4f..1b0e4bc6 100644 --- a/bazel/rules/rules_score/private/unit_design.bzl +++ b/bazel/rules/rules_score/private/unit_design.bzl @@ -25,6 +25,7 @@ verification tooling. """ load("//bazel/rules/rules_score:providers.bzl", "SphinxSourcesInfo", "UnitDesignInfo") +load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") # ============================================================================ # Private Rule Implementation @@ -46,6 +47,8 @@ def _run_puml_parser(ctx, puml_file): puml_file.path, "--fbs-output-dir", fbs_output.dirname, + "--log-level", + get_log_level(ctx), ], progress_message = "Parsing Unit Design PlantUML diagram: %s" % puml_file.short_path, ) @@ -103,24 +106,27 @@ _unit_design = rule( implementation = _unit_design_impl, doc = "Collects unit design documents and diagrams for S-CORE process compliance. " + "PlantUML files are passed through to Sphinx and parsed to FlatBuffers.", - attrs = { - "static": attr.label_list( - allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], - mandatory = False, - doc = "Static unit design diagrams (class diagrams, etc.)", - ), - "dynamic": attr.label_list( - allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], - mandatory = False, - doc = "Dynamic unit design diagrams (sequence diagrams, etc.)", - ), - "_puml_parser": attr.label( - default = Label("//plantuml/parser:parser"), - executable = True, - cfg = "exec", - doc = "PlantUML parser tool that generates FlatBuffers from .puml files", - ), - }, + attrs = dict( + { + "static": attr.label_list( + allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], + mandatory = False, + doc = "Static unit design diagrams (class diagrams, etc.)", + ), + "dynamic": attr.label_list( + allow_files = [".puml", ".plantuml", ".svg", ".rst", ".md"], + mandatory = False, + doc = "Dynamic unit design diagrams (sequence diagrams, etc.)", + ), + "_puml_parser": attr.label( + default = Label("//plantuml/parser:parser"), + executable = True, + cfg = "exec", + doc = "PlantUML parser tool that generates FlatBuffers from .puml files", + ), + }, + **VERBOSITY_ATTR + ), ) # ============================================================================ diff --git a/bazel/rules/rules_score/private/verbosity.bzl b/bazel/rules/rules_score/private/verbosity.bzl new file mode 100644 index 00000000..b5863eda --- /dev/null +++ b/bazel/rules/rules_score/private/verbosity.bzl @@ -0,0 +1,61 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Common verbosity helpers for rules_score rules. + +Provides a shared attribute definition and accessor function so that every +rules_score build rule can expose the same ``--log-level`` argument to its +underlying tools via the ``//bazel/rules/rules_score:verbosity`` build +setting. + +Usage in a rule definition:: + + load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level") + + my_rule = rule( + implementation = _impl, + attrs = dict( + ..., + **VERBOSITY_ATTR + ), + ) + + def _impl(ctx): + log_level = get_log_level(ctx) # returns "warn", "info", or "debug" + ctx.actions.run( + ..., + arguments = ["--log-level", log_level, ...], + ) +""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") + +# Private attribute that reads the verbosity build setting. +# Merge into a rule's attrs dict with ``**VERBOSITY_ATTR``. +VERBOSITY_ATTR = { + "_verbosity": attr.label( + default = Label("//bazel/rules/rules_score:verbosity"), + doc = "Verbosity level build setting (warn/info/debug).", + ), +} + +def get_log_level(ctx): + """Return the current log level string from the build setting. + + Args: + ctx: Rule context (must have ``_verbosity`` in its attrs). + + Returns: + One of ``"warn"``, ``"info"``, or ``"debug"``. + """ + return ctx.attr._verbosity[BuildSettingInfo].value diff --git a/bazel/rules/rules_score/src/rst_to_trlc.py b/bazel/rules/rules_score/src/rst_to_trlc.py index cc7fdb5a..e7276adb 100644 --- a/bazel/rules/rules_score/src/rst_to_trlc.py +++ b/bazel/rules/rules_score/src/rst_to_trlc.py @@ -13,11 +13,19 @@ """RST requirement directive to TRLC converter.""" import argparse +import logging import re import sys from pathlib import Path from typing import Any +_LEVEL_MAP = { + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} + # Maps RST directive names to TRLC types in the S-CORE requirements model. # Only directives that correspond to a concrete TRLC type in score_requirements_model.rsl # are listed here. All other directives in RST source files are silently skipped. @@ -222,10 +230,7 @@ def convert( ) directives = parse_directives(input_path.read_text(encoding="utf-8")) if not directives: - print( - f"WARNING: no supported requirement directives found in {input_path}", - file=sys.stderr, - ) + logging.warning("no supported requirement directives found in %s", input_path) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text( render_trlc(directives, pkg, ref_package or _DEFAULT_REF_PACKAGE), @@ -240,11 +245,21 @@ def convert( p.add_argument("--output-dir", type=Path, required=True) p.add_argument("--package", default=None) p.add_argument("--ref-package", default=None) + p.add_argument( + "--log-level", + choices=["error", "warn", "info", "debug"], + default="warn", + dest="log_level", + help="Log level for tool output (default: warn).", + ) args = p.parse_args() + logging.basicConfig( + level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s" + ) if not args.input_file.exists(): sys.exit(f"ERROR: file not found: {args.input_file}") output_file = args.output_dir / (args.input_file.stem + ".trlc") record_count = convert( args.input_file, output_file, package=args.package, ref_package=args.ref_package ) - print(f" {args.input_file} -> {output_file} ({record_count} record(s))") + logging.info("%s -> %s (%d record(s))", args.input_file, output_file, record_count) diff --git a/bazel/rules/rules_score/src/safety_analysis_tools.py b/bazel/rules/rules_score/src/safety_analysis_tools.py index 117774aa..6101b4af 100644 --- a/bazel/rules/rules_score/src/safety_analysis_tools.py +++ b/bazel/rules/rules_score/src/safety_analysis_tools.py @@ -35,6 +35,13 @@ logger = logging.getLogger(__name__) +_LEVEL_MAP = { + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} + LOBSTER_GENERATOR = "safety_analysis_tools" LOBSTER_SCHEMA = "lobster-act-trace" LOBSTER_VERSION = 3 @@ -221,14 +228,16 @@ def preprocess_puml( def main() -> None: - logging.basicConfig( - level=logging.INFO, - format="%(levelname)s: %(message)s", - ) - parser = argparse.ArgumentParser( description="Inline fta_metamodel.puml into FTA diagrams and extract lobster traceability.", ) + parser.add_argument( + "--log-level", + choices=["error", "warn", "info", "debug"], + default="warn", + dest="log_level", + help="Log level for tool output (default: warn).", + ) parser.add_argument( "--metamodel", required=True, @@ -251,7 +260,11 @@ def main() -> None: help="PlantUML FTA .puml files to process.", ) - _run_preprocess(parser.parse_args()) + args = parser.parse_args() + logging.basicConfig( + level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s" + ) + _run_preprocess(args) def _run_preprocess(args: argparse.Namespace) -> None: diff --git a/bazel/rules/rules_score/src/sphinx_html_merge.py b/bazel/rules/rules_score/src/sphinx_html_merge.py index 901034a9..14904ba6 100755 --- a/bazel/rules/rules_score/src/sphinx_html_merge.py +++ b/bazel/rules/rules_score/src/sphinx_html_merge.py @@ -24,12 +24,20 @@ """ import argparse +import logging import os import re import shutil import sys from pathlib import Path +_LEVEL_MAP = { + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} + # Standard Sphinx directories that should be copied # Note: _static and _sphinx_design_static are excluded for dependencies to avoid duplication @@ -51,7 +59,7 @@ def copy_html_files(src_dir, dst_dir, exclude_module_dirs=None, sibling_modules= dst_path = Path(dst_dir) if not src_path.exists(): - print(f"Warning: Source directory does not exist: {src_dir}", file=sys.stderr) + logging.warning("Source directory does not exist: %s", src_dir) return dst_path.mkdir(parents=True, exist_ok=True) @@ -96,7 +104,7 @@ def replace_static(match): dst_file.parent.mkdir(parents=True, exist_ok=True) dst_file.write_text(modified_content, encoding="utf-8") except Exception as e: - print(f"Warning: Failed to process {src_file}: {e}", file=sys.stderr) + logging.warning("Failed to process %s: %s", src_file, e) # Fallback to regular copy on error shutil.copy2(src_file, dst_file) else: @@ -141,7 +149,7 @@ def merge_html_dirs(output_dir, main_html_dir, dependencies): output_path = Path(output_dir) # First, copy the main HTML directory - print(f"Copying main HTML from {main_html_dir} to {output_dir}") + logging.info("Copying main HTML from %s to %s", main_html_dir, output_dir) copy_html_files(main_html_dir, output_dir) # Collect all dependency names for link fixing and exclusion @@ -150,7 +158,9 @@ def merge_html_dirs(output_dir, main_html_dir, dependencies): # Then copy each dependency into a subdirectory with link fixing for dep_name, dep_html_dir in dependencies: dep_output = output_path / dep_name - print(f"Copying dependency {dep_name} from {dep_html_dir} to {dep_output}") + logging.info( + "Copying dependency %s from %s to %s", dep_name, dep_html_dir, dep_output + ) # Exclude other module directories to avoid nested modules # Remove current module from the list to get actual siblings to exclude sibling_modules = set(n for n in dep_names if n != dep_name) @@ -177,16 +187,25 @@ def main(): metavar="NAME:PATH", help="Dependency HTML directory in format NAME:PATH", ) + parser.add_argument( + "--log-level", + choices=["error", "warn", "info", "debug"], + default="warn", + dest="log_level", + help="Log level for tool output (default: warn).", + ) args = parser.parse_args() + logging.basicConfig( + level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s" + ) # Parse dependencies dependencies = [] for dep_spec in args.dep: if ":" not in dep_spec: - print( - f"Error: Invalid dependency format '{dep_spec}', expected NAME:PATH", - file=sys.stderr, + logging.error( + "Invalid dependency format '%s', expected NAME:PATH", dep_spec ) return 1 @@ -196,7 +215,7 @@ def main(): # Merge the HTML directories merge_html_dirs(args.output, args.main, dependencies) - print(f"Successfully merged HTML into {args.output}") + logging.info("Successfully merged HTML into %s", args.output) return 0 diff --git a/bazel/rules/rules_score/src/sphinx_wrapper.py b/bazel/rules/rules_score/src/sphinx_wrapper.py index e3f679d3..760c7c39 100644 --- a/bazel/rules/rules_score/src/sphinx_wrapper.py +++ b/bazel/rules/rules_score/src/sphinx_wrapper.py @@ -36,11 +36,24 @@ DEFAULT_GITHUB_VERSION = "main" DEFAULT_SOURCE_DIR = "." -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(levelname)s: %(message)s", -) +_LEVEL_MAP = { + "error": logging.ERROR, + "warn": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG, +} + +# Mapping from --log-level to sphinx-build verbosity flags. +# warn → -q (suppress info; show warnings/errors only) +# info → (nothing; sphinx default output) +# debug → -vv (verbose sphinx output) +_SPHINX_VERBOSITY_FLAGS = { + "error": ["-Q"], + "warn": ["-q"], + "info": [], + "debug": ["-vv"], +} + logger = logging.getLogger(__name__) SANDBOX_PATH = re.compile(r"^.*_main/") @@ -157,6 +170,12 @@ def build_sphinx_arguments( base_arguments.extend(["-b", args.builder]) + # Apply sphinx-build verbosity flags derived from --log-level + sphinx_verbosity = _SPHINX_VERBOSITY_FLAGS.get( + getattr(args, "log_level", "warn"), [] + ) + base_arguments.extend(sphinx_verbosity) + # Forward extra options (e.g., -D flags) to Sphinx if extra_args: base_arguments.extend(extra_args) @@ -246,6 +265,13 @@ def parse_arguments() -> argparse.Namespace: default=DEFAULT_PORT, help=f"Port to use for live preview (default: {DEFAULT_PORT}). Use 0 for auto-detection.", ) + parser.add_argument( + "--log-level", + choices=["error", "warn", "info", "debug"], + default="warn", + dest="log_level", + help="Log level for wrapper and sphinx-build output (default: warn).", + ) return parser.parse_known_args() @@ -259,6 +285,9 @@ def main() -> int: """ try: args, extra_args = parse_arguments() + logging.basicConfig( + level=_LEVEL_MAP[args.log_level], format="%(levelname)s: %(message)s" + ) validate_arguments(args) # Create processor instance stdout_processor = StdoutProcessor() diff --git a/bazel/rules/rules_score/test/rst_to_trlc_test.py b/bazel/rules/rules_score/test/rst_to_trlc_test.py index 7e5d5789..2e11c5b8 100644 --- a/bazel/rules/rules_score/test/rst_to_trlc_test.py +++ b/bazel/rules/rules_score/test/rst_to_trlc_test.py @@ -738,16 +738,12 @@ def test_stkh_req_is_skipped(self): "\n" " The platform shall do something.\n" ) - import sys + import logging - buf = StringIO() - old_stderr, sys.stderr = sys.stderr, buf - try: + with self.assertLogs("root", level=logging.WARNING) as cm: out = self._convert(rst) - finally: - sys.stderr = old_stderr # stkh_req has no TRLC mapping → treated as no directives found - self.assertIn("WARNING", buf.getvalue()) + self.assertTrue(any("WARNING" in msg for msg in cm.output)) self.assertNotIn("stkh_req", out) diff --git a/plantuml/linker/BUILD b/plantuml/linker/BUILD index 5234e285..bd432299 100644 --- a/plantuml/linker/BUILD +++ b/plantuml/linker/BUILD @@ -20,7 +20,9 @@ rust_binary( deps = [ "//plantuml/parser/puml_serializer/src/fbs:component_fbs", "@crates//:clap", + "@crates//:env_logger", "@crates//:flatbuffers", + "@crates//:log", "@crates//:serde", "@crates//:serde_json", ], diff --git a/plantuml/linker/src/main.rs b/plantuml/linker/src/main.rs index c6837f6a..87ac1c88 100644 --- a/plantuml/linker/src/main.rs +++ b/plantuml/linker/src/main.rs @@ -23,10 +23,37 @@ use std::collections::HashMap; use std::fs; -use clap::Parser; +use clap::{Parser, ValueEnum}; +use env_logger::Builder; use component_fbs::component as fb_component; +// --------------------------------------------------------------------------- +// Log level +// --------------------------------------------------------------------------- + +/// CLI-visible log level (mirrors the parser's convention). +#[derive(Copy, Clone, ValueEnum, Debug)] +enum CliLogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl CliLogLevel { + fn to_level_filter(self) -> log::LevelFilter { + match self { + CliLogLevel::Error => log::LevelFilter::Error, + CliLogLevel::Warn => log::LevelFilter::Warn, + CliLogLevel::Info => log::LevelFilter::Info, + CliLogLevel::Debug => log::LevelFilter::Debug, + CliLogLevel::Trace => log::LevelFilter::Trace, + } + } +} + // --------------------------------------------------------------------------- // CLI // --------------------------------------------------------------------------- @@ -48,6 +75,10 @@ struct Args { /// Output JSON file path #[arg(long, default_value = "plantuml_links.json")] output: String, + + /// Log level: error, warn, info, debug, trace + #[arg(long, value_enum, default_value = "warn")] + log_level: CliLogLevel, } // --------------------------------------------------------------------------- @@ -198,6 +229,9 @@ fn generate_links(diagrams: &[DiagramInfo]) -> Vec { fn main() -> Result<(), Box> { let args = Args::parse(); + Builder::new() + .filter_level(args.log_level.to_level_filter()) + .init(); if args.fbs_files.is_empty() { return Err("No .fbs.bin files provided. Use --fbs-files ...".into()); @@ -207,7 +241,7 @@ fn main() -> Result<(), Box> { for fbs_path in &args.fbs_files { match read_diagram(fbs_path) { Ok(diagram) => { - eprintln!( + log::info!( "Read {} components from {}", diagram.components.len(), diagram.source_file @@ -215,18 +249,18 @@ fn main() -> Result<(), Box> { diagrams.push(diagram); } Err(e) => { - eprintln!("Warning: skipping {fbs_path}: {e}"); + log::warn!("Skipping {}: {}", fbs_path, e); } } } let links = generate_links(&diagrams); - eprintln!("Generated {} link(s)", links.len()); + log::info!("Generated {} link(s)", links.len()); let output = LinksJson { links }; let json = serde_json::to_string_pretty(&output)?; fs::write(&args.output, &json)?; - eprintln!("Written to {}", args.output); + log::debug!("Written to {}", args.output); Ok(()) } diff --git a/plantuml/parser/puml_serializer/src/fbs/BUILD b/plantuml/parser/puml_serializer/src/fbs/BUILD index 3de84117..033eaff3 100644 --- a/plantuml/parser/puml_serializer/src/fbs/BUILD +++ b/plantuml/parser/puml_serializer/src/fbs/BUILD @@ -37,6 +37,7 @@ rust_library( "--allow=clippy::extra-unused-lifetimes", "--allow=clippy::missing-safety-doc", "--allow=clippy::needless-lifetimes", + "--allow=mismatched_lifetime_syntaxes", ], visibility = [ "//visibility:public", @@ -69,6 +70,7 @@ rust_library( "--allow=clippy::extra-unused-lifetimes", "--allow=clippy::missing-safety-doc", "--allow=clippy::needless-lifetimes", + "--allow=mismatched_lifetime_syntaxes", ], visibility = [ "//plantuml/parser:__subpackages__", @@ -102,6 +104,7 @@ rust_library( "--allow=clippy::extra-unused-lifetimes", "--allow=clippy::missing-safety-doc", "--allow=clippy::needless-lifetimes", + "--allow=mismatched_lifetime_syntaxes", ], visibility = [ "//plantuml/parser:__subpackages__", diff --git a/validation/core/BUILD b/validation/core/BUILD index 06b468ea..413edfce 100644 --- a/validation/core/BUILD +++ b/validation/core/BUILD @@ -50,6 +50,8 @@ rust_binary( deps = [ ":validation", "@crates//:clap", + "@crates//:env_logger", + "@crates//:log", ], ) diff --git a/validation/core/src/main.rs b/validation/core/src/main.rs index 31a5156e..8894e14d 100644 --- a/validation/core/src/main.rs +++ b/validation/core/src/main.rs @@ -20,7 +20,8 @@ use std::fs; use std::mem; use std::process; -use clap::Parser; +use clap::{Parser, ValueEnum}; +use env_logger::Builder; use validation::{ validate_bazel_component, validate_component_class, BazelArchitecture, BazelInput, BazelReader, ClassDiagramIndex, ClassDiagramInputs, ClassDiagramReader, ComponentDiagramArchitecture, @@ -28,6 +29,28 @@ use validation::{ SelectedValidator, ValidatorSpec, ALL_VALIDATORS, }; +/// CLI-visible log level (mirrors the parser/linker convention). +#[derive(Copy, Clone, ValueEnum, Debug)] +enum CliLogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl CliLogLevel { + fn to_level_filter(self) -> log::LevelFilter { + match self { + CliLogLevel::Error => log::LevelFilter::Error, + CliLogLevel::Warn => log::LevelFilter::Warn, + CliLogLevel::Info => log::LevelFilter::Info, + CliLogLevel::Debug => log::LevelFilter::Debug, + CliLogLevel::Trace => log::LevelFilter::Trace, + } + } +} + #[derive(Parser, Debug)] #[command(name = "validation")] #[command(version = "1.0")] @@ -49,6 +72,10 @@ struct Args { /// with code 0. Intended for use during development (maturity=development). #[arg(long, default_value_t = false)] warn_on_errors: bool, + + /// Log level: error, warn, info, debug, trace + #[arg(long, value_enum, default_value = "warn")] + log_level: CliLogLevel, } struct ValidationCliInputs { @@ -231,7 +258,7 @@ fn finish_validation( details ); if warn_on_errors { - eprintln!("WARNING: {output}"); + log::warn!("{}", output); Ok(()) } else { Err(output) @@ -263,8 +290,12 @@ fn write_log(path: &str, errors: &Errors) -> Result<(), String> { } fn main() { - if let Err(msg) = run(Args::parse()) { - eprintln!("{msg}"); + let args = Args::parse(); + Builder::new() + .filter_level(args.log_level.to_level_filter()) + .init(); + if let Err(msg) = run(args) { + log::error!("{}", msg); process::exit(1); } }