diff --git a/src/BUILD b/src/BUILD index a43a5b3f..facfdab3 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,12 +32,14 @@ py_binary( name = "codechecker_script", srcs = ["codechecker_script.py"], visibility = ["//visibility:public"], + deps = [":common"], ) 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/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/codechecker_script.py b/src/codechecker_script.py index dedf159a..95154335 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -21,10 +21,15 @@ import os import plistlib import re -import shlex -import subprocess -import sys - +from common import ( + fail, + parse, + check_results, + stage, + execute, + build_env, + setup_logging, +) START_PATH = r"\/(?:(?!\.\s+)\S)+" BAZEL_PATHS = { @@ -66,66 +71,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": - 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") @@ -144,52 +89,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 +100,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,101 +240,32 @@ 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( + 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) -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(): """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 new file mode 100644 index 00000000..34c1ecf3 --- /dev/null +++ b/src/common.py @@ -0,0 +1,201 @@ +""" +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 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() + 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(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} {input_dir}" + ) + # 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}") diff --git a/src/per_file.bzl b/src/per_file.bzl index a12a0997..d4ced9ca 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 = {} @@ -117,6 +114,7 @@ def _run_code_checker( ctx.attr._per_file_script[DefaultInfo].files_to_run, ], arguments = [ + "--mode=Run", "--codechecker", info.codechecker.path, "--commands", @@ -136,8 +134,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, @@ -207,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 @@ -235,29 +236,89 @@ 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] + + 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]), ), ] @@ -284,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. " + @@ -310,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 9a79e46c..d80f6299 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -17,18 +17,24 @@ """ import argparse -from dataclasses import dataclass import os import re import shutil import subprocess -import sys +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 + check_results, fail, parse, setup_logging, build_env +) @dataclass 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 @@ -39,7 +45,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): @@ -48,54 +56,58 @@ 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( + "--severities", required=False, help="Severities to check" ) - 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( - "--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) - 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 + 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, + severities=args.severities, + codechecker_bin=os.path.realpath(args.codechecker or "/"), compile_commands=args.commands, codechecker_args=args.analyze, config_file=args.config, @@ -105,7 +117,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=os.path.realpath(args.clang), + clang_tidy=os.path.realpath(args.clang_tidy), ) @@ -160,19 +174,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 @@ -188,7 +189,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") @@ -198,7 +199,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, @@ -209,7 +211,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, @@ -217,19 +220,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): @@ -300,10 +295,39 @@ def main(): Main function of CodeChecker wrapper """ cfg = parse_args() - _create_compile_commands_json_with_absolute_paths(cfg) - _run_codechecker(cfg) - _move_output_files(cfg) + setup_logging(cfg.verbosity, cfg.log_file) + 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, + ) + elif cfg.execution_mode == "Test": + check_results(cfg.data_dir, cfg.log_file, cfg.severities) + 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", )