From 9134bf6c80fd33a78a91272ebf07abd9976ce5f0 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Tue, 22 Sep 2026 14:29:42 +0200 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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 175ce6e389177da7694cdd172175c7ad79a2c457 Mon Sep 17 00:00:00 2001 From: "F.Tibor" Date: Thu, 8 Oct 2026 12:15:40 +0200 Subject: [PATCH 6/6] use named args in mono script, refactor parse function, add docstrings, add execution mode to per-file --- src/codechecker_script.py | 15 ++++++------ src/common.py | 50 ++++++++++++++++++++++++++++++++++----- src/per_file.bzl | 1 + src/per_file_script.py | 16 +++++++++---- 4 files changed, 65 insertions(+), 17 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..25bf028f 100644 --- a/src/common.py +++ b/src/common.py @@ -30,7 +30,15 @@ def setup_logging(verbosity, codechecker_log): def build_env(env, log, clang, clang_tidy): - """Return environment""" + """ + Return environment to run codechecker in. + + Args: + env (list): Environment variables + log (str): Path to the log file + clang (str): Path to the clang executable + clang_tidy (str): Path to the clang-tidy executable + """ new_env = os.environ.copy() for entry in env: if "=" not in entry: @@ -53,7 +61,15 @@ def build_env(env, log, clang, clang_tidy): def execute(codechecker_log, cmd, env=None, codes=None): - """Execute CodeChecker commands""" + """ + Execute CodeChecker commands + + Args: + codechecker_log (str): Path to the log file + cmd (str): Command to execute + env (dict): Environment variables + codes (list): List of expected exit codes + """ if codes is None: codes = [0] with subprocess.Popen( @@ -117,15 +133,30 @@ def stage(title, method="info"): 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""" +def parse( + input_dir, output_dir, codechecker, config, env, log, clang, clang_tidy +): + """ + Run CodeChecker parse commands + + Args: + input_dir (str): Path to the input directory + output_dir (str): Path to the output directory + codechecker (str): Path to the CodeChecker binary + config (str): Path to the CodeChecker configuration file + env (list): Environment variables + log (str): Path to the log file + clang (str): Path to the clang executable + clang_tidy (str): Path to the clang-tidy executable + """ 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 = ( @@ -147,7 +178,14 @@ def parse(output_dir, codechecker, config, env, log, clang, clang_tidy): def check_results(output_dir, log, severities): - """Check/verify CodeChecker results""" + """ + Filter CodeChecker results by severity level, fail on provided severities. + + Args: + output_dir (str): Path to the output directory + log (str): Path to the log file + severities (str): Comma separated list of defect severities to fail on + """ stage("Checking result:") # Get results file and read it result_file = output_dir + "/result.txt" diff --git a/src/per_file.bzl b/src/per_file.bzl index 259ea1fe..b1c0db57 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", diff --git a/src/per_file_script.py b/src/per_file_script.py index b9c44890..adcf316d 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -29,6 +29,7 @@ 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 @@ -49,7 +50,7 @@ def parse_args(argv=None): parser = argparse.ArgumentParser( 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" ) @@ -93,6 +94,7 @@ def parse_args(argv=None): ] return Config( + execution_mode=args.mode, codechecker_bin=os.path.realpath(args.codechecker), compile_commands=args.commands, codechecker_args=args.analyze, @@ -282,9 +284,15 @@ 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) + else: + fail( + cfg.log_file, + f"Wrong codechecker script mode: {cfg.execution_mode}", + ) if __name__ == "__main__":