From 9134bf6c80fd33a78a91272ebf07abd9976ce5f0 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 22 Sep 2026 14:29:42 +0200 Subject: [PATCH 1/7] refactor: move parse and related function to common This is to reuse these functions from the per_file script.0 --- src/BUILD | 9 ++ src/codechecker_script.py | 189 +++----------------------------------- src/common.py | 176 +++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 175 deletions(-) create mode 100644 src/common.py diff --git a/src/BUILD b/src/BUILD index a43a5b3f..ffedfedd 100644 --- a/src/BUILD +++ b/src/BUILD @@ -12,6 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//python:py_library.bzl", "py_library") + +py_library( + name = "common", + srcs = ["common.py"], + imports = ["."], + visibility = ["//visibility:public"], +) # Tool filter compile_commands.json file py_binary( @@ -24,6 +32,7 @@ py_binary( name = "codechecker_script", srcs = ["codechecker_script.py"], visibility = ["//visibility:public"], + deps = [":common"], ) py_binary( diff --git a/src/codechecker_script.py b/src/codechecker_script.py index dedf159a..e732bf88 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -21,10 +21,7 @@ import os import plistlib import re -import shlex -import subprocess -import sys - +from common import fail, parse, check_results, stage, execute, build_env START_PATH = r"\/(?:(?!\.\s+)\S)+" BAZEL_PATHS = { @@ -66,47 +63,6 @@ def parse_args(argv=None): return args -def fail(codechecker_log, message, exit_code=1): - """Print error message and return exit code""" - logging.error(message) - print() - print("*" * 50) - print("codechecker script execution FAILED!") - if codechecker_log: - print(f"See: {codechecker_log}") - print("*" * 50) - try: - with open(codechecker_log, encoding="utf-8") as log_file: - print(log_file.read()) - except IOError: - print("File not accessible") - else: - print(message) - print("*" * 50) - print() - sys.exit(exit_code) - - -def read_file(codechecker_log, filename): - """Read text file and return its contents""" - if not os.path.isfile(filename): - fail(codechecker_log, f"File not found: {filename}") - with open(filename, encoding="utf-8") as handle: - return handle.read() - - -def separator(method="info"): - """Print log separator line to logging.info() or other logging methods""" - getattr(logging, method)("#" * 23) - - -def stage(title, method="info"): - """Print stage title into log""" - separator(method) - getattr(logging, method)("### " + title) - separator(method) - - def setup(verbosity, codechecker_log): """Setup logging parameters for execution session""" if verbosity == "INFO": @@ -119,9 +75,8 @@ def setup(verbosity, codechecker_log): if codechecker_log: logging.basicConfig( - filename=codechecker_log, - level=log_level, - format=log_format) + filename=codechecker_log, level=log_level, format=log_format + ) else: logging.basicConfig(level=log_level, format=log_format) @@ -144,52 +99,6 @@ def input_data(args): logging.debug("") -def execute(codechecker_log, cmd, env=None, codes=None): - """Execute CodeChecker commands""" - if codes is None: - codes = [0] - with subprocess.Popen( - cmd, - env=env, - shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) as process: - stdout, stderr = process.communicate() - stdout = stdout.decode("utf-8") - stderr = stderr.decode("utf-8") - if process.returncode not in codes: - fail(codechecker_log, - f"\ncommand: {cmd}\nstdout: {stdout}\nstderr: {stderr}\n") - logging.debug("Executing: %s", cmd) - # logging.debug("Output:\n\n%s\n", stdout) - return stdout - - -def build_env(args): - """Return environment""" - env = os.environ.copy() - for entry in args.env: - if "=" not in entry: - fail(args.log, f"Environment entry is not KEY=VALUE: {entry}") - key, value = entry.split("=", 1) - env[key] = value - # Note: This is a workaround, CodeChecker requires the PATH to be set - if "PATH" not in env: - env["PATH"] = "/bin" - if env.get("CC_ANALYZERS_FROM_PATH"): - logging.debug("CC_ANALYZERS_FROM_PATH is set: use analyzers from PATH") - elif env.get("CC_ANALYZER_BIN"): - logging.debug("CC_ANALYZER_BIN is set by the configuration") - else: - env["CC_ANALYZER_BIN"] = ( - f"clangsa:{args.clang};clang-tidy:{args.clang_tidy}" - ) - logging.debug("env: %s", str(env)) - return env - - def prepare(codechecker_files): """Prepare CodeChecker execution environment""" stage("CodeChecker files:") @@ -201,7 +110,7 @@ def prepare(codechecker_files): def analyze(args): """Run CodeChecker analyze command""" stage("CodeChecker analyze:") - env = build_env(args) + env = build_env(args.env, args.log, args.clang, args.clang_tidy) output = execute( args.log, f"{args.codechecker} analyzers --details", @@ -341,95 +250,25 @@ def update_file_paths(codechecker_files): resolve_symlinks(codechecker_files) -def parse(args): - """Run CodeChecker parse commands""" - stage("CodeChecker parse:") - env = build_env(args) - logging.info("CodeChecker parse -e json") - codechecker_parse = ( - f"{args.codechecker} parse --config " - f"{args.config} {args.output}/data" - ) - # Save results to JSON file - command = ( - f"{codechecker_parse} --export=json > " - f"{args.output}/result.json" - ) - execute(args.log, command, env=env, codes=[0, 2]) - # Save results as HTML report - logging.info("CodeChecker parse -e html") - command = ( - codechecker_parse - + " --export=html --output=" - + args.output - + "/report" - ) - execute(args.log, command, env=env, codes=[0, 2]) - # Save results to text file - logging.info("CodeChecker parse to text result") - result_file = args.output + "/result.txt" - command = codechecker_parse + " > " + result_file - execute(args.log, command, env=env, codes=[0, 2]) - logging.info("Result:\n\n%s\n", read_file(args.log, result_file)) - - def run(args): """Perform all steps for "bazel build" phase""" prepare(args.output) analyze(args) - parse(args) + parse( + args.output, + args.codechecker, + args.config, + args.env, + args.log, + args.clang, + args.clang_tidy, + ) update_file_paths(args.output) -def check_results(args): - """Check/verify CodeChecker results""" - stage("Checking result:") - # Get results file and read it - result_file = args.output + "/result.txt" - logging.info("Find CodeChecker results in bazel-bin") - logging.info(" all artifacts: %s/", args.output) - logging.info(" HTML report: %s/report/index.html", args.output) - logging.info(" result file: %s", result_file) - results = read_file(args.log, result_file) - logging.info("Results: \n\n%s\n", results) - # Collect defect severities to detect - if args.severities is None: - fail(args.log, - "CodeChecker defect severities are invalid: " - f"{str(args.severities)}") - severities = shlex.split(args.severities) - # Add HIGH severity by default - if not severities: - severities.append("HIGH") - # We should always detect CRITICAL defects - if "CRITICAL" not in severities: - severities.append("CRITICAL") - logging.debug("Severities: %s", str(severities)) - issues = dict.fromkeys(severities, 0) - logging.debug("Issues: %s", str(issues)) - # Grep results for defects according to severities - for issue in issues: - found = re.findall(rf"^{issue} .* (\d+)", results, re.M) - defects = sum(int(number) for number in found) - logging.debug(" %s : %s = %d", issue, str(found), defects) - issues[issue] = defects - logging.info("Defects: %s", str(issues)) - # Check collected defects - passed = True - conclusion = "" - for issue, num in issues.items(): - if num > 0: - passed = False - conclusion += f"{issue:>15} : {num}\n" - if passed: - logging.info("No defects found by CodeChecker") - else: - fail(args.log, f"CodeChecker found defects:\n{conclusion}") - - def test(args): """Perform all steps for "bazel test" phase""" - check_results(args) + check_results(args.output, args.log, args.severities) def main(): diff --git a/src/common.py b/src/common.py new file mode 100644 index 00000000..fc035f6a --- /dev/null +++ b/src/common.py @@ -0,0 +1,176 @@ +""" +Common utilities for running codechecker. +This module is shared between the "per_file_script.py" +and "codechecker_script.py" files. +""" + +import logging +import shlex +import subprocess +import sys +import os +import re + +def build_env(env, log, clang, clang_tidy): + """Return environment""" + new_env = os.environ.copy() + for entry in env: + if "=" not in entry: + fail(log, f"Environment entry is not KEY=VALUE: {entry}") + key, value = entry.split("=", 1) + new_env[key] = value + # Note: This is a workaround, CodeChecker requires the PATH to be set + if "PATH" not in new_env: + new_env["PATH"] = "/bin" + if new_env.get("CC_ANALYZERS_FROM_PATH"): + logging.debug("CC_ANALYZERS_FROM_PATH is set: use analyzers from PATH") + elif new_env.get("CC_ANALYZER_BIN"): + logging.debug("CC_ANALYZER_BIN is set by the configuration") + else: + new_env["CC_ANALYZER_BIN"] = ( + f"clangsa:{clang};clang-tidy:{clang_tidy}" + ) + logging.debug("env: %s", str(new_env)) + return new_env + + +def execute(codechecker_log, cmd, env=None, codes=None): + """Execute CodeChecker commands""" + if codes is None: + codes = [0] + with subprocess.Popen( + cmd, + env=env, + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as process: + stdout, stderr = process.communicate() + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") + if process.returncode not in codes: + fail( + codechecker_log, + f"\ncommand: {cmd}\nstdout: {stdout}\nstderr: {stderr}\n", + ) + logging.debug("Executing: %s", cmd) + # logging.debug("Output:\n\n%s\n", stdout) + return stdout + + +def read_file(codechecker_log, filename): + """Read text file and return its contents""" + if not os.path.isfile(filename): + fail(codechecker_log, f"File not found: {filename}") + with open(filename, encoding="utf-8") as handle: + return handle.read() + + +def fail(codechecker_log, message, exit_code=1): + """Print error message and return exit code""" + logging.error(message) + print() + print("*" * 50) + print("codechecker script execution FAILED!") + if codechecker_log: + print(f"See: {codechecker_log}") + print("*" * 50) + try: + with open(codechecker_log, encoding="utf-8") as log_file: + print(log_file.read()) + except IOError: + print("File not accessible") + else: + print(message) + print("*" * 50) + print() + sys.exit(exit_code) + + +def separator(method="info"): + """Print log separator line to logging.info() or other logging methods""" + getattr(logging, method)("#" * 23) + + +def stage(title, method="info"): + """Print stage title into log""" + separator(method) + getattr(logging, method)("### " + title) + separator(method) + +# pylint: disable=too-many-arguments,too-many-positional-arguments +def parse(output_dir, codechecker, config, env, log, clang, clang_tidy): + """Run CodeChecker parse commands""" + stage("CodeChecker parse:") + env = build_env(env, log, clang, clang_tidy) + logging.info("CodeChecker parse -e json") + codechecker_parse = ( + f"{codechecker} parse --config " + f"{config} {output_dir}/data" + ) + # Save results to JSON file + command = ( + f"{codechecker_parse} --export=json > " f"{output_dir}/result.json" + ) + execute(log, command, env=env, codes=[0, 2]) + # Save results as HTML report + logging.info("CodeChecker parse -e html") + command = ( + codechecker_parse + " --export=html --output=" + output_dir + "/report" + ) + execute(log, command, env=env, codes=[0, 2]) + # Save results to text file + logging.info("CodeChecker parse to text result") + result_file = output_dir + "/result.txt" + command = codechecker_parse + " > " + result_file + execute(log, command, env=env, codes=[0, 2]) + logging.info("Result:\n\n%s\n", read_file(log, result_file)) + + +def check_results(output_dir, log, severities): + """Check/verify CodeChecker results""" + stage("Checking result:") + # Get results file and read it + result_file = output_dir + "/result.txt" + logging.info("Find CodeChecker results in bazel-bin") + logging.info(" all artifacts: %s/", output_dir) + logging.info(" HTML report: %s/report/index.html", output_dir) + logging.info(" result file: %s", result_file) + results = read_file(log, result_file) + logging.info("Results: \n\n%s\n", results) + # Collect defect severities to detect + if severities is None: + fail( + log, + "CodeChecker defect severities are invalid: " + f"{str(severities)}", + ) + severities = shlex.split(severities) # pyright: ignore[reportArgumentType] + # Add HIGH severity by default + if not severities: + severities.append("HIGH") + # We should always detect CRITICAL defects + if "CRITICAL" not in severities: + severities.append("CRITICAL") + logging.debug("Severities: %s", str(severities)) + issues = dict.fromkeys(severities, 0) + logging.debug("Issues: %s", str(issues)) + # Grep results for defects according to severities + for issue in issues: + found = re.findall(rf"^{issue} .* (\d+)", results, re.M) + defects = sum(int(number) for number in found) + logging.debug(" %s : %s = %d", issue, str(found), defects) + issues[issue] = defects + logging.info("Defects: %s", str(issues)) + # Check collected defects + passed = True + conclusion = "" + for issue, num in issues.items(): + if num > 0: + passed = False + conclusion += f"{issue:>15} : {num}\n" + if passed: + logging.info("No defects found by CodeChecker") + else: + fail(log, f"CodeChecker found defects:\n{conclusion}") From 09b02e3c49146092ce0b42635adc25735c912557 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 22 Sep 2026 15:44:47 +0200 Subject: [PATCH 2/7] Make per_file use shared components --- src/BUILD | 1 + src/per_file_script.py | 20 ++++++-------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/BUILD b/src/BUILD index ffedfedd..facfdab3 100644 --- a/src/BUILD +++ b/src/BUILD @@ -39,6 +39,7 @@ py_binary( name = "per_file_script", srcs = ["per_file_script.py"], visibility = ["//visibility:public"], + deps = [":common"], ) # The following are flags and default values for clang_tidy_aspect diff --git a/src/per_file_script.py b/src/per_file_script.py index 9a79e46c..5aa0b748 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -22,7 +22,7 @@ import re import shutil import subprocess -import sys +from common import fail @dataclass @@ -217,19 +217,11 @@ def _run_codechecker(cfg: Config) -> None: except subprocess.CalledProcessError as e: log(cfg, e.output.decode() if e.output else "") if e.returncode == 1 or e.returncode >= 128: - _display_error(cfg, e.returncode) - - -def _display_error(cfg: Config, ret_code: int) -> None: - """ - Display the log file, and exit with 1 - """ - # Log and exit on error - print("===-----------------------------------------------------===") - print(f"[ERROR]: CodeChecker returned with {ret_code}!") - with open(cfg.log_file, "r", encoding="utf-8") as log_file: - print(log_file.read()) - sys.exit(1) + fail( + cfg.log_file, + f"CodeChecker failed with return code {e.returncode}\n", + e.returncode, + ) def _move_output_files(cfg: Config): From be5e413198f6511c24724e7c881f950a5e247110 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 6 Oct 2026 15:26:28 +0200 Subject: [PATCH 3/7] Move the logging setup function out of codechecker-script --- src/codechecker_script.py | 30 ++++++++++-------------------- src/common.py | 18 ++++++++++++++++++ src/per_file_script.py | 6 +++++- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/codechecker_script.py b/src/codechecker_script.py index e732bf88..bb4a18cb 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -21,7 +21,15 @@ import os import plistlib import re -from common import fail, parse, check_results, stage, execute, build_env +from common import ( + fail, + parse, + check_results, + stage, + execute, + build_env, + setup_logging, +) START_PATH = r"\/(?:(?!\.\s+)\S)+" BAZEL_PATHS = { @@ -63,24 +71,6 @@ def parse_args(argv=None): return args -def setup(verbosity, codechecker_log): - """Setup logging parameters for execution session""" - if verbosity == "INFO": - log_level = logging.INFO - elif verbosity == "WARN": - log_level = logging.WARN - else: - log_level = logging.DEBUG - log_format = "[codechecker] %(levelname)5s: %(message)s" - - if codechecker_log: - logging.basicConfig( - filename=codechecker_log, level=log_level, format=log_format - ) - else: - logging.basicConfig(level=log_level, format=log_format) - - def input_data(args): """Print out input (external) parameters""" stage("CodeChecker input data:", "debug") @@ -274,7 +264,7 @@ def test(args): def main(): """Main function""" args = parse_args() - setup(args.verbosity, args.log) + setup_logging(args.verbosity, args.log) input_data(args) try: if args.mode == "Run": diff --git a/src/common.py b/src/common.py index fc035f6a..4b36deed 100644 --- a/src/common.py +++ b/src/common.py @@ -11,6 +11,24 @@ import os import re +def setup_logging(verbosity, codechecker_log): + """Setup logging parameters for execution session""" + if verbosity == "INFO": + log_level = logging.INFO + elif verbosity == "WARN": + log_level = logging.WARN + else: + log_level = logging.DEBUG + log_format = "[codechecker] %(levelname)5s: %(message)s" + + if codechecker_log: + logging.basicConfig( + filename=codechecker_log, level=log_level, format=log_format + ) + else: + logging.basicConfig(level=log_level, format=log_format) + + def build_env(env, log, clang, clang_tidy): """Return environment""" new_env = os.environ.copy() diff --git a/src/per_file_script.py b/src/per_file_script.py index 5aa0b748..36af6283 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -22,7 +22,7 @@ import re import shutil import subprocess -from common import fail +from common import fail, setup_logging @dataclass @@ -40,6 +40,7 @@ class Config: # pylint: disable=too-many-instance-attributes metadata_file: str analyzer_plist_paths: list analyzer_executables_env_var: str + verbosity: str def parse_args(argv=None): @@ -51,6 +52,7 @@ def parse_args(argv=None): parser.add_argument( "--codechecker", required=True, help="Path to CodeChecker binary" ) + parser.add_argument("--verbosity", default="INFO", help="Log level") parser.add_argument( "--commands", required=True, help="Path to compile_commands.json" ) @@ -106,6 +108,7 @@ def parse_args(argv=None): metadata_file=args.metadata, analyzer_plist_paths=analyzer_plist_paths, analyzer_executables_env_var=analyzer_executables_env_var, + verbosity=args.verbosity, ) @@ -292,6 +295,7 @@ def main(): Main function of CodeChecker wrapper """ cfg = parse_args() + setup_logging(cfg.verbosity, cfg.log_file) _create_compile_commands_json_with_absolute_paths(cfg) _run_codechecker(cfg) _move_output_files(cfg) From 060baefa70d485300d39903486fdd2219a77e480 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 6 Oct 2026 15:47:07 +0200 Subject: [PATCH 4/7] Use shared environment function --- src/per_file.bzl | 9 ++++----- src/per_file_script.py | 46 +++++++++++++++--------------------------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/per_file.bzl b/src/per_file.bzl index a12a0997..259ea1fe 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -96,9 +96,6 @@ def _run_code_checker( analyzer_output_paths = "clangsa," + clangsa_plist.path + \ ";clang-tidy," + clang_tidy_plist.path - analyzer_executables = "clangsa:" + info.clangsa.path + \ - ";clang-tidy:" + info.clang_tidy.path - # Convert the list of "NAME=value" strings into a dict. # Entries without "=" are ignored. env = {} @@ -136,8 +133,10 @@ def _run_code_checker( codechecker_metadata.path, "--analyzer_plists", analyzer_output_paths, - "--analyzer_executables", - analyzer_executables, + "--clang", + info.clangsa.path, + "--clang_tidy", + info.clang_tidy.path, ], mnemonic = "CodeChecker", env = env, diff --git a/src/per_file_script.py b/src/per_file_script.py index 36af6283..8bd3f828 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -22,7 +22,7 @@ import re import shutil import subprocess -from common import fail, setup_logging +from common import fail, setup_logging, build_env @dataclass @@ -39,8 +39,9 @@ class Config: # pylint: disable=too-many-instance-attributes skip_file: str metadata_file: str analyzer_plist_paths: list - analyzer_executables_env_var: str verbosity: str + clang: str + clang_tidy: str def parse_args(argv=None): @@ -77,9 +78,12 @@ def parse_args(argv=None): help="Semicolon-separated list of analyzer,plist_path pairs", ) parser.add_argument( - "--analyzer_executables", - default="", - help="Semicolon-separated list of name:path pairs", + "--clang", + help="Path for clang executable", + ) + parser.add_argument( + "--clang_tidy", + help="Path for clang-tidy executable", ) args = parser.parse_args(argv) @@ -87,14 +91,6 @@ def parse_args(argv=None): analyzer_plist_paths = [ item.split(",") for item in args.analyzer_plists.split(";") ] - analyzer_executables_env_var = ";".join( - f"{name}:{os.path.realpath(path)}" - for name, path in [ - pair.split(":", 1) - for pair in args.analyzer_executables.split(";") - if pair - ] - ) return Config( codechecker_bin=os.path.realpath(args.codechecker), @@ -107,8 +103,9 @@ def parse_args(argv=None): skip_file=args.skip, metadata_file=args.metadata, analyzer_plist_paths=analyzer_plist_paths, - analyzer_executables_env_var=analyzer_executables_env_var, verbosity=args.verbosity, + clang=args.clang, + clang_tidy=args.clang_tidy, ) @@ -163,19 +160,6 @@ def _create_compile_commands_json_with_absolute_paths(cfg: Config): new_file.write(new_content) -def _get_codechecker_env(cfg: Config) -> dict[str, str]: - """ - Returns the environment for running CodeChecker - """ - cc_env = os.environ.copy() - # Note: This is a workaround, CodeChecker requires the PATH to be set - if "PATH" not in cc_env: - cc_env["PATH"] = "/bin" - # Overwrite analyzer paths - cc_env["CC_ANALYZER_BIN"] = cfg.analyzer_executables_env_var - return cc_env - - def _run_codechecker(cfg: Config) -> None: """ Runs CodeChecker analyze @@ -191,7 +175,7 @@ def _run_codechecker(cfg: Config) -> None: + [absolute_path] ) - cc_env = _get_codechecker_env(cfg) + cc_env = build_env("", cfg.log_file, cfg.clang, cfg.clang_tidy) env_prefix = " ".join(f"{key}={cc_env[key]}" for key in sorted(cc_env)) log(cfg, f"CodeChecker command: {env_prefix} {' '.join(codechecker_cmd)}\n") log(cfg, "===---------------------------------------------===\n") @@ -201,7 +185,8 @@ def _run_codechecker(cfg: Config) -> None: result = subprocess.run( ["echo", "$PATH"], shell=True, - env=_get_codechecker_env(cfg), + # Env vars are set in bazel + env=build_env("", cfg.log_file, cfg.clang, cfg.clang_tidy), capture_output=True, text=True, check=False, @@ -212,7 +197,8 @@ def _run_codechecker(cfg: Config) -> None: with open(cfg.log_file, "a", encoding="utf-8") as log_file: subprocess.run( codechecker_cmd, - env=_get_codechecker_env(cfg), + # Env vars are set in bazel + env=build_env("", cfg.log_file, cfg.clang, cfg.clang_tidy), stdout=log_file, stderr=log_file, check=True, From 1d43ff8ed7dd5cd601b1bbe40869863697a8d701 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 6 Oct 2026 16:37:44 +0200 Subject: [PATCH 5/7] Fix clang not being runnable in RHEL --- src/per_file_script.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/per_file_script.py b/src/per_file_script.py index 8bd3f828..b9c44890 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -104,8 +104,8 @@ def parse_args(argv=None): metadata_file=args.metadata, analyzer_plist_paths=analyzer_plist_paths, verbosity=args.verbosity, - clang=args.clang, - clang_tidy=args.clang_tidy, + clang=os.path.realpath(args.clang), + clang_tidy=os.path.realpath(args.clang_tidy), ) From 52e99c79663c3308e9e88bf3f57581460763e104 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Wed, 26 Aug 2026 05:47:45 +0200 Subject: [PATCH 6/7] Add parse step to per-file Move logging into common.py Fix lint issues Update expected action number (since we added an extra parse action) Undo changes to codechecker script --- src/codechecker_script.py | 15 +++++---- src/common.py | 13 ++++++-- src/per_file.bzl | 48 +++++++++++++++++++++++++++ src/per_file_script.py | 69 +++++++++++++++++++++++++++++---------- test/unit/caching/BUILD | 4 +-- 5 files changed, 120 insertions(+), 29 deletions(-) diff --git a/src/codechecker_script.py b/src/codechecker_script.py index bb4a18cb..95154335 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -245,13 +245,14 @@ def run(args): prepare(args.output) analyze(args) parse( - args.output, - args.codechecker, - args.config, - args.env, - args.log, - args.clang, - args.clang_tidy, + input_dir=args.output, + output_dir=args.output, + codechecker=args.codechecker, + config=args.config, + env=args.env, + log=args.log, + clang=args.clang, + clang_tidy=args.clang_tidy, ) update_file_paths(args.output) diff --git a/src/common.py b/src/common.py index 4b36deed..34c1ecf3 100644 --- a/src/common.py +++ b/src/common.py @@ -118,14 +118,21 @@ def stage(title, method="info"): separator(method) # pylint: disable=too-many-arguments,too-many-positional-arguments -def parse(output_dir, codechecker, config, env, log, clang, clang_tidy): - """Run CodeChecker parse commands""" +def parse(input_dir, output_dir, codechecker, config, env, log, clang, + clang_tidy): + """Run CodeChecker parse commands + + Args: + input_dir: Directory containing the analyzer plists to parse. + output_dir: Directory where the result files (json, html, txt) are + written. + """ stage("CodeChecker parse:") env = build_env(env, log, clang, clang_tidy) logging.info("CodeChecker parse -e json") codechecker_parse = ( f"{codechecker} parse --config " - f"{config} {output_dir}/data" + f"{config} {input_dir}" ) # Save results to JSON file command = ( diff --git a/src/per_file.bzl b/src/per_file.bzl index 259ea1fe..7e522eeb 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -114,6 +114,7 @@ def _run_code_checker( ctx.attr._per_file_script[DefaultInfo].files_to_run, ], arguments = [ + "--mode=Run", "--codechecker", info.codechecker.path, "--commands", @@ -206,6 +207,7 @@ def _per_file_impl(ctx): info = ctx.attr.toolchain[platform_common.ToolchainInfo].codecheckerinfo else: info = ctx.toolchains["//:toolchain_type"].codecheckerinfo + for target in ctx.attr.targets: if not CcInfo in target: continue @@ -234,6 +236,52 @@ def _per_file_impl(ctx): sources_and_headers, ) all_files += outputs + + # Parse action: collect all plists into a directory and run + # CodeChecker parse to produce result.txt, result.json, HTML report + codechecker_files = ctx.actions.declare_directory( + ctx.label.name + "/parse", + ) + codechecker_parse_log = ctx.actions.declare_file( + ctx.label.name + "/codechecker_parse.log", + ) + + # Build arguments for the parse action + # The data dir is where the per-file analyze actions put their plists + # All plists are in /data/, derive path from first plist + #data_dir_path = plist_and_metadata_files[0].dirname if plist_and_metadata_files else "" + + ctx.actions.run( + inputs = all_files + [config_file], + outputs = [codechecker_files, codechecker_parse_log], + executable = per_file_script, + tools = [ + info.runfiles, + ctx.attr._per_file_script[DefaultInfo].files_to_run, + ], + arguments = [ + "--mode", + "Parse", + "--codechecker", + info.codechecker.path, + "--data_dir", + codechecker_files.path, + "--log", + codechecker_parse_log.path, + "--config", + config_file.path, + "--clang", + info.clangsa.path, + "--clang_tidy", + info.clang_tidy.path, + ], + mnemonic = "CodeCheckerParse", + use_default_shell_env = True, + progress_message = "CodeChecker parse %s" % str(ctx.label), + ) + + all_files += [codechecker_files, codechecker_parse_log] + ctx.actions.write( output = ctx.outputs.test_script, is_executable = True, diff --git a/src/per_file_script.py b/src/per_file_script.py index b9c44890..5c5ff862 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -17,18 +17,23 @@ """ import argparse -from dataclasses import dataclass import os import re import shutil import subprocess -from common import fail, setup_logging, build_env +from dataclasses import dataclass +# pylint outside bazel cannot follow the dependency graph +# This should be removed when pylint is integrated into bazel +from common import ( # pylint: disable=no-name-in-module + fail, parse, setup_logging, build_env +) @dataclass class Config: # pylint: disable=too-many-instance-attributes """Configuration parsed from command-line arguments.""" + execution_mode: str codechecker_bin: str compile_commands: str codechecker_args: str @@ -50,31 +55,32 @@ def parse_args(argv=None): description="CodeChecker per-file analysis wrapper" ) + parser.add_argument("--mode", required=True, help="Execution mode") parser.add_argument( - "--codechecker", required=True, help="Path to CodeChecker binary" + "--codechecker", required=False, help="Path to CodeChecker binary" ) parser.add_argument("--verbosity", default="INFO", help="Log level") parser.add_argument( - "--commands", required=True, help="Path to compile_commands.json" + "--commands", required=False, help="Path to compile_commands.json" ) parser.add_argument( "--analyze", default="", help="CodeChecker analyze arguments" ) - parser.add_argument("--config", required=True, help="Path to config file") + parser.add_argument("--config", required=False, help="Path to config file") parser.add_argument( "--data_dir", required=True, help="Output directory for CodeChecker" ) parser.add_argument( - "--file", required=True, help="Path to the file to be analyzed" + "--file", required=False, help="Path to the file to be analyzed" ) - parser.add_argument("--log", required=True, help="Path to the log file") - parser.add_argument("--skip", required=True, help="Path to the skip file") + parser.add_argument("--log", required=False, help="Path to the log file") + parser.add_argument("--skip", required=False, help="Path to the skip file") parser.add_argument( - "--metadata", required=True, help="Path to the metadata file" + "--metadata", required=False, help="Path to the metadata file" ) parser.add_argument( "--analyzer_plists", - required=True, + required=False, help="Semicolon-separated list of analyzer,plist_path pairs", ) parser.add_argument( @@ -88,12 +94,15 @@ def parse_args(argv=None): args = parser.parse_args(argv) - analyzer_plist_paths = [ - item.split(",") for item in args.analyzer_plists.split(";") - ] + analyzer_plist_paths = [] + if args.analyzer_plists: + analyzer_plist_paths = [ + item.split(",") for item in args.analyzer_plists.split(";") + ] return Config( - codechecker_bin=os.path.realpath(args.codechecker), + execution_mode=args.mode, + codechecker_bin=os.path.realpath(args.codechecker or "/"), compile_commands=args.commands, codechecker_args=args.analyze, config_file=args.config, @@ -282,10 +291,36 @@ def main(): """ cfg = parse_args() setup_logging(cfg.verbosity, cfg.log_file) - _create_compile_commands_json_with_absolute_paths(cfg) - _run_codechecker(cfg) - _move_output_files(cfg) + if cfg.execution_mode == "Run": + _create_compile_commands_json_with_absolute_paths(cfg) + _run_codechecker(cfg) + _move_output_files(cfg) + elif cfg.execution_mode == "Parse": + with open(cfg.log_file, "a", encoding="utf-8"): + pass + parse( + input_dir=cfg.data_dir + "/..", + output_dir=cfg.data_dir, + codechecker=cfg.codechecker_bin, + config=cfg.config_file, + env="", + log=cfg.log_file, + clang=cfg.clang, + clang_tidy=cfg.clang_tidy, + ) + else: + fail( + cfg.log_file, + f"Wrong codechecker script mode: {cfg.execution_mode}", + ) if __name__ == "__main__": main() + + +# I have conserved this comment from the original bash script +# The sed commands are commented out, so we won't implement them +# sed -i -e "s|.*execroot/bazel_codechecker/||g" \ +# $CLANG_TIDY_PLIST +# sed -i -e "s|.*execroot/bazel_codechecker/||g" $CLANGSA_PLIST diff --git a/test/unit/caching/BUILD b/test/unit/caching/BUILD index c173b56b..79ce3a17 100644 --- a/test/unit/caching/BUILD +++ b/test/unit/caching/BUILD @@ -33,14 +33,14 @@ caching_test( caching_test( name = "caching_per_file_test", - expected_action_count = 1, + expected_action_count = 2, file_to_modify = "secondary.cc", target_name = "per_file_caching", ) caching_test( name = "caching_per_file_ctu_test", - expected_action_count = 2, + expected_action_count = 3, file_to_modify = "secondary.cc", target_name = "per_file_caching_ctu", ) From 9952165594a827f5801254a1b447d80be52fcb3e Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Wed, 26 Aug 2026 05:47:45 +0200 Subject: [PATCH 7/7] Add severities to per_file rule Move logging into common.py Fix lint issues Update expected action number (since we added an extra parse action) Undo changes to codechecker script --- src/codechecker.bzl | 1 + src/per_file.bzl | 51 ++++++++++++++++++++++++++++-------------- src/per_file_script.py | 9 +++++++- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/codechecker.bzl b/src/codechecker.bzl index e4fd8909..3ea88a40 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -372,6 +372,7 @@ def codechecker_test( options = analyze, skip = skip, config = config, + severities = severities, toolchain = toolchain, tags = codechecker_tags, **kwargs diff --git a/src/per_file.bzl b/src/per_file.bzl index 7e522eeb..d4ced9ca 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -282,29 +282,43 @@ def _per_file_impl(ctx): all_files += [codechecker_files, codechecker_parse_log] + launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh") ctx.actions.write( - output = ctx.outputs.test_script, + output = launcher, + content = """#!/bin/bash + exec {tool} --mode=Test \ + --data_dir '{codechecker_files}' --severities '{severities}' \ + --clang '{clang}' --clang_tidy '{clang_tidy}' + """.format( + tool = per_file_script.executable.short_path, + codechecker_files = codechecker_files.short_path, + severities = " ".join(ctx.attr.severities), + clang = info.clangsa.short_path, + clang_tidy = info.clang_tidy.short_path, + ), is_executable = True, - content = """ - DATA_DIR=$(dirname {dirname}) - # ls -la $DATA_DIR/data - # find $DATA_DIR/data -name *.plist -exec sed -i -e "s|.*execroot/codechecker_bazel/||g" {{}} \\; - # cat $DATA_DIR/data/test-src-lib.cc_clangsa.plist - echo "Running: CodeChecker parse $DATA_DIR/data" - $(realpath {codechecker}) parse $DATA_DIR/data - """.format(dirname = ctx.outputs.test_script.short_path, codechecker = info.codechecker.short_path), - ) - files = depset( - direct = all_files, ) + + # Return test script and all required files run_files = [ - ctx.outputs.test_script, + launcher, + codechecker_files, ] + info.runfiles.to_list() + all_files + all_runfiles = ctx.runfiles(files = run_files) + + # Add runfiles from the py_binary target (for common.py etc.) + all_runfiles = all_runfiles.merge( + ctx.attr._per_file_script[DefaultInfo].default_runfiles, + ) + return [ DefaultInfo( - files = files, - runfiles = ctx.runfiles(files = run_files), - executable = ctx.outputs.test_script, + files = depset(all_files), + runfiles = all_runfiles, + executable = launcher, + ), + OutputGroupInfo( + codechecker_files = depset([codechecker_files]), ), ] @@ -331,6 +345,10 @@ per_file_test = rule( default = "", #"@platforms//os:linux", doc = "Platform to build for", ), + "severities": attr.string_list( + default = ["HIGH"], + doc = "List of defect severities: HIGH, MEDIUM, LOW, STYLE etc", + ), "skip": attr.string_list( default = [], doc = "List of skip/ignore file rules. " + @@ -357,7 +375,6 @@ per_file_test = rule( } | version_specific_attributes(), outputs = { "compile_commands": "%{name}/compile_commands.json", - "test_script": "%{name}/test_script.sh", }, test = True, toolchains = ["//:toolchain_type"], diff --git a/src/per_file_script.py b/src/per_file_script.py index 5c5ff862..d80f6299 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -25,7 +25,7 @@ # pylint outside bazel cannot follow the dependency graph # This should be removed when pylint is integrated into bazel from common import ( # pylint: disable=no-name-in-module - fail, parse, setup_logging, build_env + check_results, fail, parse, setup_logging, build_env ) @@ -34,6 +34,7 @@ class Config: # pylint: disable=too-many-instance-attributes """Configuration parsed from command-line arguments.""" execution_mode: str + severities: str codechecker_bin: str compile_commands: str codechecker_args: str @@ -73,6 +74,9 @@ def parse_args(argv=None): parser.add_argument( "--file", required=False, help="Path to the file to be analyzed" ) + parser.add_argument( + "--severities", required=False, help="Severities to check" + ) parser.add_argument("--log", required=False, help="Path to the log file") parser.add_argument("--skip", required=False, help="Path to the skip file") parser.add_argument( @@ -102,6 +106,7 @@ def parse_args(argv=None): return Config( execution_mode=args.mode, + severities=args.severities, codechecker_bin=os.path.realpath(args.codechecker or "/"), compile_commands=args.commands, codechecker_args=args.analyze, @@ -308,6 +313,8 @@ def main(): clang=cfg.clang, clang_tidy=cfg.clang_tidy, ) + elif cfg.execution_mode == "Test": + check_results(cfg.data_dir, cfg.log_file, cfg.severities) else: fail( cfg.log_file,