diff --git a/src/BUILD b/src/BUILD index ba0c85dd..6a2b6482 100644 --- a/src/BUILD +++ b/src/BUILD @@ -14,7 +14,7 @@ load(":codechecker_toolchain.bzl", "codechecker_toolchain") # Tool filter compile_commands.json file -# In bazel 6 we use our own python toolchain, +# For targets using WORKSPACE files we use our own python toolchain, # therefore we cannot load from rules_python. # buildifier: disable=native-py py_binary( @@ -23,12 +23,26 @@ py_binary( visibility = ["//visibility:public"], ) -# Build & Test script template -exports_files( - [ - "codechecker_script.py", - "per_file_script.py", - ], +# For targets using WORKSPACE files we use our own python toolchain, +# therefore we cannot load from rules_python. +# buildifier: disable=native-py +py_binary( + name = "per_file_script", + srcs = ["per_file_script.py"], + main = "per_file_script.py", + # Bazel 6 doesn't see this otherwise + visibility = ["//visibility:public"], +) + +# For targets using WORKSPACE files we use our own python toolchain, +# therefore we cannot load from rules_python. +# buildifier: disable=native-py +py_binary( + name = "codechecker_script", + srcs = ["codechecker_script.py"], + main = "codechecker_script.py", + # Bazel 6 doesn't see this otherwise + visibility = ["//visibility:public"], ) # The following are flags and default values for clang_tidy_aspect diff --git a/src/codechecker.bzl b/src/codechecker.bzl index 0c6eba57..aea19e64 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -96,24 +96,26 @@ def _codechecker_impl(ctx): info = ctx.toolchains["//src:toolchain_type"].codecheckerinfo codechecker_files = ctx.actions.declare_directory(ctx.label.name + "/codechecker-files") - ctx.actions.expand_template( - template = ctx.file._codechecker_script_template, - output = ctx.outputs.codechecker_script, - is_executable = True, - substitutions = { - "{Mode}": "Run", - "{Verbosity}": "DEBUG", - "{clang_bin}": info.clangsa.path, - "{clang_tidy_bin}": info.clang_tidy.path, - "{codechecker_analyze}": " ".join(ctx.attr.analyze), - "{codechecker_bin}": info.codechecker.path, - "{codechecker_config}": config_file.path, - "{codechecker_env}": codechecker_env, - "{codechecker_files}": codechecker_files.path, - "{codechecker_log}": ctx.outputs.codechecker_log.path, - "{codechecker_skipfile}": ctx.outputs.codechecker_skipfile.path, - "{compile_commands}": ctx.outputs.codechecker_commands.path, - }, + + bazel_constants_json = ctx.actions.declare_file(ctx.label.name + "_bazel_build_constants.json") + codechecker_data = { + "Mode": "Run", + "Verbosity": "DEBUG", + "clang_bin": info.clangsa.path, + "clang_tidy_bin": info.clang_tidy.path, + "codechecker_analyze": ctx.attr.analyze, # this is a list + "codechecker_bin": info.codechecker.path, + "codechecker_config": config_file.path, + "codechecker_env": codechecker_env, + "codechecker_files": codechecker_files.path, + "codechecker_log": ctx.outputs.codechecker_log.path, + "codechecker_skipfile": ctx.outputs.codechecker_skipfile.path, + "compile_commands": ctx.outputs.codechecker_commands.path, + } + + ctx.actions.write( + output = bazel_constants_json, + content = json.encode_indent(codechecker_data), ) ctx.actions.run( @@ -122,20 +124,18 @@ def _codechecker_impl(ctx): info.codechecker, info.clangsa, info.clang_tidy, - ctx.outputs.codechecker_script, ctx.outputs.codechecker_commands, ctx.outputs.codechecker_skipfile, config_file, + bazel_constants_json, ] + source_files, ), outputs = [ codechecker_files, ctx.outputs.codechecker_log, ], - executable = ctx.outputs.codechecker_script, - arguments = [], - # executable = python_path(ctx), - # arguments = [ctx.outputs.codechecker_script.path], + executable = ctx.executable._codechecker_script, + arguments = [bazel_constants_json.path], mnemonic = "CodeChecker", progress_message = "CodeChecker %s" % str(ctx.label), # use_default_shell_env = True, @@ -148,7 +148,6 @@ def _codechecker_impl(ctx): ctx.outputs.codechecker_skipfile, config_file, codechecker_files, - ctx.outputs.codechecker_script, ctx.outputs.codechecker_log, ] + source_files @@ -190,9 +189,10 @@ codechecker = rule( ], doc = "List of compilable targets which should be checked.", ), - "_codechecker_script_template": attr.label( - default = ":codechecker_script.py", - allow_single_file = True, + "_codechecker_script": attr.label( + default = ":codechecker_script", + executable = True, + cfg = "exec", ), "_compile_commands_filter": attr.label( allow_files = True, @@ -204,7 +204,6 @@ codechecker = rule( outputs = { "codechecker_commands": "%{name}/codechecker_commands.json", "codechecker_log": "%{name}/codechecker.log", - "codechecker_script": "%{name}/codechecker_script.py", "codechecker_skipfile": "%{name}/codechecker_skipfile.cfg", "compile_commands": "%{name}/compile_commands.json", }, @@ -233,34 +232,48 @@ def _codechecker_test_impl(ctx): info = ctx.toolchains["//src:toolchain_type"].codecheckerinfo - # Create test script from template - ctx.actions.expand_template( - template = ctx.file._codechecker_script_template, - output = ctx.outputs.codechecker_test_script, + bazel_constants_json = ctx.actions.declare_file(ctx.label.name + "_bazel_test_constants.json") + ctx.actions.write( + output = bazel_constants_json, + content = json.encode_indent({ + "Mode": "Test", + "Severities": " ".join(ctx.attr.severities), + "Verbosity": "INFO", + "clang_bin": info.clangsa.short_path, + "clang_tidy_bin": info.clang_tidy.short_path, + "codechecker_bin": info.codechecker.short_path, + "codechecker_files": codechecker_files.short_path, + }, indent = " ") + "\n", + is_executable = False, + ) + + launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh") + ctx.actions.write( + output = launcher, + content = """#!/bin/bash + exec {tool} {constants} + """.format( + tool = ctx.executable._codechecker_script.short_path, + constants = bazel_constants_json.short_path, + ), is_executable = True, - substitutions = { - "{Mode}": "Test", - "{Severities}": " ".join(ctx.attr.severities), - "{Verbosity}": "INFO", - "{clang_bin}": info.clangsa.short_path, - "{clang_tidy_bin}": info.clang_tidy.short_path, - "{codechecker_bin}": info.codechecker.short_path, - "{codechecker_files}": codechecker_files.short_path, - }, ) # Return test script and all required files run_files = default_runfiles + [ - ctx.outputs.codechecker_test_script, + ctx.executable._codechecker_script, info.codechecker, info.clang_tidy, info.clangsa, + bazel_constants_json, ] + run_files = ctx.runfiles(files = run_files) + run_files = run_files.merge(ctx.attr._codechecker_script[DefaultInfo].default_runfiles) return [ DefaultInfo( files = depset(all_files), - runfiles = ctx.runfiles(files = run_files), - executable = ctx.outputs.codechecker_test_script, + runfiles = run_files, + executable = launcher, ), ] @@ -296,9 +309,10 @@ _codechecker_test = rule( cfg = platforms_transition, doc = "List of compilable targets which should be checked.", ), - "_codechecker_script_template": attr.label( - default = ":codechecker_script.py", - allow_single_file = True, + "_codechecker_script": attr.label( + default = ":codechecker_script", + executable = True, + cfg = "exec", ), "_compile_commands_filter": attr.label( allow_files = True, @@ -310,9 +324,7 @@ _codechecker_test = rule( outputs = { "codechecker_commands": "%{name}/codechecker_commands.json", "codechecker_log": "%{name}/codechecker.log", - "codechecker_script": "%{name}/codechecker_script.py", "codechecker_skipfile": "%{name}/codechecker_skipfile.cfg", - "codechecker_test_script": "%{name}/codechecker_test_script.py", "compile_commands": "%{name}/compile_commands.json", }, toolchains = [ diff --git a/src/codechecker_script.py b/src/codechecker_script.py index cde29ca0..854a2d9e 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 Ericsson AB # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,20 +24,25 @@ import shlex import subprocess import sys +import json +from pathlib import Path -EXECUTION_MODE = "{Mode}" -VERBOSITY = "{Verbosity}" -CODECHECKER_PATH = os.path.realpath("{codechecker_bin}") -CLANG_PATH = os.path.realpath("{clang_bin}") -CLANG_TIDY_PATH = os.path.realpath("{clang_tidy_bin}") -CODECHECKER_SKIPFILE = "{codechecker_skipfile}" -CODECHECKER_CONFIG = "{codechecker_config}" -CODECHECKER_ANALYZE = "{codechecker_analyze}" -CODECHECKER_FILES = "{codechecker_files}" -CODECHECKER_LOG = "{codechecker_log}" -CODECHECKER_SEVERITIES = "{Severities}" -CODECHECKER_ENV = "{codechecker_env}" -COMPILE_COMMANDS = "{compile_commands}" +if __name__ == "__main__": + # Arguments should only be parsed, if this script is the entry point + CONSTANTS: dict = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + EXECUTION_MODE = CONSTANTS.get("Mode", "Run") + VERBOSITY = CONSTANTS.get("Verbosity", "INFO") + CODECHECKER_PATH = os.path.realpath(CONSTANTS["codechecker_bin"]) + CLANG_PATH = os.path.realpath(CONSTANTS["clang_bin"]) + CLANG_TIDY_PATH = os.path.realpath(CONSTANTS["clang_tidy_bin"]) + CODECHECKER_SKIPFILE = CONSTANTS.get("codechecker_skipfile") + CODECHECKER_CONFIG = CONSTANTS.get("codechecker_config") + CODECHECKER_ANALYZE: list[str] = CONSTANTS.get("codechecker_analyze", []) + CODECHECKER_FILES = CONSTANTS["codechecker_files"] + CODECHECKER_LOG = CONSTANTS.get("codechecker_log") + CODECHECKER_SEVERITIES = CONSTANTS.get("Severities", []) + CODECHECKER_ENV = CONSTANTS.get("codechecker_env") + COMPILE_COMMANDS = CONSTANTS.get("compile_commands") START_PATH = r"\/(?:(?!\.\s+)\S)+" BAZEL_PATHS = { @@ -204,7 +207,7 @@ def analyze(): command = ( f"{CODECHECKER_PATH} analyze --skip={CODECHECKER_SKIPFILE} " f"{COMPILE_COMMANDS} --output={CODECHECKER_FILES}/data " - f"--config {CODECHECKER_CONFIG} {CODECHECKER_ANALYZE}" + f"--config {CODECHECKER_CONFIG} {' '.join(CODECHECKER_ANALYZE)}" ) # FIXME: Workaround "CodeChecker simply remove compiler-rt include path". # This can be removed once codechecker 6.16.0 is used. diff --git a/src/per_file.bzl b/src/per_file.bzl index a63316c7..10183120 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -37,7 +37,9 @@ def _run_code_checker( env_vars, compile_commands_json, compilation_context, - sources_and_headers): + sources_and_headers, + skipfile, + bazel_constants): # Define Plist and log file names data_dir = ctx.attr.name + "/data" file_name_params = (data_dir, src.path.replace("/", "-")) @@ -50,38 +52,25 @@ def _run_code_checker( clangsa_plist = ctx.actions.declare_file(clangsa_plist_file_name) codechecker_log = ctx.actions.declare_file(codechecker_log_file_name) - # Create skipfile - config = ctx.actions.declare_file( - "{}/{}_skipfile".format(*file_name_params), - ) - ctx.actions.write( - output = config, - content = "\n".join(ctx.attr.skip), - ) - # TODO: Consider using aliases so we don't have to type //src: everywhere. info = ctx.toolchains["//src:toolchain_type"].codecheckerinfo + mutual_inputs = [ + compile_commands_json, + config_file, + skipfile, + info.codechecker, + info.clang_tidy, + info.clangsa, + bazel_constants, + ] if "--ctu" in options: - inputs = [ - compile_commands_json, - config_file, - config, - info.codechecker, - info.clangsa, - info.clang_tidy, - ] + sources_and_headers + inputs = mutual_inputs + sources_and_headers else: # NOTE: we collect only headers, so CTU may not work! headers = depset(transitive = target[SourceFilesInfo].headers.to_list()) - inputs = depset([ - compile_commands_json, - config_file, + inputs = depset(mutual_inputs + [ src, - config, - info.codechecker, - info.clangsa, - info.clang_tidy, ], transitive = [headers]) outputs = [clang_tidy_plist, clangsa_plist, codechecker_log] @@ -96,13 +85,12 @@ def _run_code_checker( ctx.actions.run( inputs = inputs, outputs = outputs, - executable = ctx.outputs.per_file_script, + executable = ctx.executable._per_file_script, arguments = [ - info.codechecker.path, + bazel_constants.path, data_dir, src.path, codechecker_log.path, - config.path, analyzer_output_paths, analyzer_executables, ], @@ -153,20 +141,43 @@ def _collect_all_sources_and_headers(ctx): all_files += headers return all_files -def _create_wrapper_script(ctx, options, compile_commands_json, config_file): +def _create_constants_file( + ctx, + options, + compile_commands_json, + config_file, + skipfile): options_str = "" for item in options: options_str += item + " " - ctx.actions.expand_template( - template = ctx.file._per_file_script_template, - output = ctx.outputs.per_file_script, - is_executable = True, - substitutions = { - "{codechecker_args}": options_str, - "{compile_commands_json}": compile_commands_json.path, - "{config_file}": config_file.path, - }, + + # Define variables to use in a file + bazel_constants = ctx.actions.declare_file( + "{}/bazel_constants.json".format(ctx.attr.name), + ) + info = ctx.toolchains["//src:toolchain_type"].codecheckerinfo + ctx.actions.write( + output = bazel_constants, + content = json.encode_indent({ + "codechecker_args": options_str, + "codechecker_bin": info.codechecker.path, + "compile_commands_json": compile_commands_json.path, + "config_file": config_file.path, + "skipfile": skipfile.path, + }), + ) + return bazel_constants + +def _create_skipfile(ctx): + # Create skipfile + skipfile = ctx.actions.declare_file( + "{}/skipfile".format(ctx.attr.name), ) + ctx.actions.write( + output = skipfile, + content = "\n".join(ctx.attr.skip), + ) + return skipfile def _per_file_impl(ctx): compile_commands = None @@ -181,7 +192,14 @@ def _per_file_impl(ctx): options = ctx.attr.default_options + ctx.attr.options config_file, env_vars = get_config_file(ctx) all_files = [compile_commands, config_file] - _create_wrapper_script(ctx, options, compile_commands, config_file) + skipfile = _create_skipfile(ctx) + bazel_constants = _create_constants_file( + ctx, + options, + compile_commands, + config_file, + skipfile, + ) for target in ctx.attr.targets: if not CcInfo in target: continue @@ -206,6 +224,8 @@ def _per_file_impl(ctx): compile_commands, compilation_context, sources_and_headers, + skipfile, + bazel_constants, ) all_files += outputs ctx.actions.write( @@ -261,14 +281,14 @@ per_file_test = rule( ], doc = "List of compilable targets which should be checked.", ), - "_per_file_script_template": attr.label( - default = ":per_file_script.py", - allow_single_file = True, + "_per_file_script": attr.label( + default = ":per_file_script", + executable = True, + cfg = "exec", ), }, outputs = { "compile_commands": "%{name}/compile_commands.json", - "per_file_script": "%{name}/per_file_script.py", "test_script": "%{name}/test_script.sh", }, test = True, diff --git a/src/per_file_script.py b/src/per_file_script.py index dc5a1476..f0fe333e 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Copyright 2023 Ericsson AB # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,24 +21,23 @@ import shutil import subprocess import sys +import json +from pathlib import Path -COMPILE_COMMANDS_JSON: str = "{compile_commands_json}" -COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" -CODECHECKER_ARGS: str = "{codechecker_args}" -CONFIG_FILE: str = "{config_file}" -SKIP_FILE: str = sys.argv[5] -CODECHECKER_BIN = os.path.realpath(sys.argv[1]) +CONSTANTS = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +COMPILE_COMMANDS_ABSOLUTE: str = f'{CONSTANTS["compile_commands_json"]}.abs' +CODECHECKER_BIN = os.path.realpath(CONSTANTS["codechecker_bin"]) # The output directory for CodeChecker DATA_DIR = sys.argv[2] # The file to be analyzed FILE_PATH = sys.argv[3] LOG_FILE = sys.argv[4] # List of pairs of analyzers and their plist files -ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[6].split(";")] +ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[5].split(";")] ANALYZER_EXECUTABLES_ENV_VAR = ";".join( f"{name}:{os.path.realpath(path)}" for name, path in [ - pair.split(":", 1) for pair in sys.argv[7].split(";") if pair + pair.split(":", 1) for pair in sys.argv[6].split(";") if pair ] ) @@ -76,7 +73,7 @@ def _create_compile_commands_json_with_absolute_paths(): of the files. """ with open( - COMPILE_COMMANDS_JSON, "r", encoding="utf-8" + CONSTANTS["compile_commands_json"], "r", encoding="utf-8" ) as original_file, open( COMPILE_COMMANDS_ABSOLUTE, "w", encoding="utf-8" ) as new_file: @@ -96,6 +93,7 @@ def _get_codechecker_env() -> dict[str, str]: cc_env = os.environ.copy() # Overwrite analyzer paths cc_env["CC_ANALYZER_BIN"] = ANALYZER_EXECUTABLES_ENV_VAR + print(ANALYZER_EXECUTABLES_ENV_VAR) return cc_env @@ -105,11 +103,11 @@ def _run_codechecker() -> None: """ codechecker_cmd: list[str] = ( [CODECHECKER_BIN, "analyze"] - + CODECHECKER_ARGS.split() + + CONSTANTS["codechecker_args"].split() + ["--output=" + DATA_DIR] + ["--file=*/" + FILE_PATH] - + ["--skip", SKIP_FILE] - + ["--config", CONFIG_FILE] + + ["--skip", CONSTANTS["skipfile"]] + + ["--config", CONSTANTS["config_file"]] + [COMPILE_COMMANDS_ABSOLUTE] ) log(f"CodeChecker command: {' '.join(codechecker_cmd)}\n") @@ -191,7 +189,7 @@ def main(): """ Main function of CodeChecker wrapper """ - if len(sys.argv) != 8: + if len(sys.argv) != 7: print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths()