diff --git a/src/BUILD b/src/BUILD index 029908cd..0d375553 100644 --- a/src/BUILD +++ b/src/BUILD @@ -19,12 +19,16 @@ py_binary( visibility = ["//visibility:public"], ) -# Build & Test script template -exports_files( - [ - "codechecker_script.py", - "per_file_script.py", - ], +py_binary( + name = "codechecker_script", + srcs = ["codechecker_script.py"], + visibility = ["//visibility:public"], +) + +py_binary( + name = "per_file_script", + srcs = ["per_file_script.py"], + 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 6030b201..cb546828 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -27,7 +27,6 @@ load( ) load( "common.bzl", - "python_path", "python_toolchain_type", "version_specific_attributes", ) @@ -97,29 +96,28 @@ def _codechecker_impl(ctx): config_file, codechecker_env = get_config_file(ctx) 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", - "{PythonPath}": python_path(ctx), # "/usr/bin/env python3", - "{codechecker_bin}": CODECHECKER_BIN_PATH, - "{compile_commands}": ctx.outputs.codechecker_commands.path, - "{codechecker_skipfile}": ctx.outputs.codechecker_skipfile.path, - "{codechecker_config}": config_file.path, - "{codechecker_analyze}": " ".join(ctx.attr.analyze), - "{codechecker_files}": codechecker_files.path, - "{codechecker_log}": ctx.outputs.codechecker_log.path, - "{codechecker_env}": codechecker_env, - }, - ) + codechecker_script = ctx.actions.declare_file(ctx.label.name + "/codechecker_script") + ctx.actions.symlink( + output = codechecker_script, + target_file = ctx.executable._codechecker_script, + ) + cmd_args = ctx.actions.args() + cmd_args.add("--mode", "Run") + cmd_args.add("--verbosity", "DEBUG") + cmd_args.add("--codechecker_path", CODECHECKER_BIN_PATH) + cmd_args.add("--commands", ctx.outputs.codechecker_commands.path) + cmd_args.add("--skip", ctx.outputs.codechecker_skipfile.path) + cmd_args.add("--config", config_file.path) + if len(ctx.attr.analyze) != 0: + cmd_args.add("--analyze", "'" + " ".join(ctx.attr.analyze) + "'") + cmd_args.add("--files", codechecker_files.path) + cmd_args.add("--log", ctx.outputs.codechecker_log.path) + cmd_args.add("--env", codechecker_env) ctx.actions.run( inputs = depset( [ - ctx.outputs.codechecker_script, + codechecker_script, ctx.outputs.codechecker_commands, ctx.outputs.codechecker_skipfile, config_file, @@ -129,10 +127,9 @@ def _codechecker_impl(ctx): codechecker_files, ctx.outputs.codechecker_log, ], - executable = ctx.outputs.codechecker_script, - arguments = [], - # executable = python_path(ctx), - # arguments = [ctx.outputs.codechecker_script.path], + executable = codechecker_script, + tools = [ctx.attr._codechecker_script[DefaultInfo].files_to_run], + arguments = [cmd_args], mnemonic = "CodeChecker", progress_message = "CodeChecker %s" % str(ctx.label), # use_default_shell_env = True, @@ -193,16 +190,18 @@ codechecker = rule( cfg = "host", default = ":compile_commands_filter", ), - "_codechecker_script_template": attr.label( - default = ":codechecker_script.py", - allow_single_file = True, + "_codechecker_script": attr.label( + allow_files = True, + executable = True, + cfg = "target", + default = ":codechecker_script", ), }, outputs = { "compile_commands": "%{name}/compile_commands.json", "codechecker_commands": "%{name}/codechecker_commands.json", "codechecker_skipfile": "%{name}/codechecker_skipfile.cfg", - "codechecker_script": "%{name}/codechecker_script.py", + "codechecker_script": "%{name}/codechecker_script", "codechecker_log": "%{name}/codechecker.log", }, toolchains = [python_toolchain_type()], @@ -225,28 +224,40 @@ def _codechecker_test_impl(ctx): if not codechecker_files: fail("Execution results required for codechecker test are not available") - # Create test script from template - ctx.actions.expand_template( - template = ctx.file._codechecker_script_template, - output = ctx.outputs.codechecker_test_script, + # Create test script + codechecker_test_script = ctx.actions.declare_file(ctx.label.name + "/codechecker_test_script") + ctx.actions.symlink( + output = codechecker_test_script, + target_file = ctx.executable._codechecker_script, + ) + + launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh") + ctx.actions.write( + output = launcher, + content = """#!/bin/bash + exec {tool} --mode=Test --verbosity=INFO \ + --codechecker_path '{codechecker_path}' \ + --files '{codechecker_files}' --severities '{severities}' + """.format( + tool = ctx.outputs.codechecker_test_script.short_path, + codechecker_path = CODECHECKER_BIN_PATH, + codechecker_files = codechecker_files.short_path, + severities = " ".join(ctx.attr.severities), + ), is_executable = True, - substitutions = { - "{Mode}": "Test", - "{Verbosity}": "INFO", - "{PythonPath}": python_path(ctx), # "/usr/bin/env python3", - "{codechecker_bin}": CODECHECKER_BIN_PATH, - "{codechecker_files}": codechecker_files.short_path, - "{Severities}": " ".join(ctx.attr.severities), - }, ) # Return test script and all required files - run_files = default_runfiles + [ctx.outputs.codechecker_test_script] + run_files = default_runfiles + [ctx.outputs.codechecker_test_script, launcher] + all_runfiles = ctx.runfiles(files = run_files) + + # Add runfiles from the py_binary target: + all_runfiles = all_runfiles.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 = all_runfiles, + executable = launcher, ), ] @@ -270,9 +281,11 @@ _codechecker_test = rule( cfg = "host", default = ":compile_commands_filter", ), - "_codechecker_script_template": attr.label( - default = ":codechecker_script.py", - allow_single_file = True, + "_codechecker_script": attr.label( + allow_files = True, + executable = True, + cfg = "target", + default = ":codechecker_script", ), "severities": attr.string_list( default = ["HIGH"], @@ -297,9 +310,9 @@ _codechecker_test = rule( "compile_commands": "%{name}/compile_commands.json", "codechecker_commands": "%{name}/codechecker_commands.json", "codechecker_skipfile": "%{name}/codechecker_skipfile.cfg", - "codechecker_script": "%{name}/codechecker_script.py", + "codechecker_script": "%{name}/codechecker_script", "codechecker_log": "%{name}/codechecker.log", - "codechecker_test_script": "%{name}/codechecker_test_script.py", + "codechecker_test_script": "%{name}/codechecker_test_script", }, toolchains = [python_toolchain_type()], test = True, diff --git a/src/codechecker_script.py b/src/codechecker_script.py index dd87a74a..07494b39 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -1,5 +1,3 @@ -#!{PythonPath} - # Copyright 2023 Ericsson AB # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,19 +24,39 @@ import shlex import subprocess import sys - - -EXECUTION_MODE = "{Mode}" -VERBOSITY = "{Verbosity}" -CODECHECKER_PATH = "{codechecker_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}" +import argparse + +parser = argparse.ArgumentParser(description="CodeChecker Bazel Wrapper") + +parser.add_argument("--mode", required=True, help="Execution mode") +parser.add_argument("--verbosity", default="INFO", help="Log level") +parser.add_argument( + "--codechecker_path", required=True, help="CodeChecker path" +) +parser.add_argument("--commands", help="Compile commands json") +parser.add_argument("--skip", help="Skipfile path") +parser.add_argument("--config", help="Config file path") +parser.add_argument("--analyze", default="", help="Analysis options") +parser.add_argument( + "--files", help="Folder where CodeChecker will store its results" +) +parser.add_argument("--log", help="Log file path") +parser.add_argument("--env", help="Environment for CodeChecker") +parser.add_argument("--severities", help="List of severities to fail on") + +args = parser.parse_args() + +EXECUTION_MODE = args.mode +VERBOSITY = args.verbosity +CODECHECKER_PATH = args.codechecker_path +COMPILE_COMMANDS = args.commands +CODECHECKER_SKIPFILE = args.skip +CODECHECKER_CONFIG = args.config +CODECHECKER_ANALYZE = args.analyze +CODECHECKER_FILES = args.files +CODECHECKER_LOG = args.log +CODECHECKER_ENV = args.env +CODECHECKER_SEVERITIES = args.severities START_PATH = r"\/(?:(?!\.\s+)\S)+" BAZEL_PATHS = { @@ -49,7 +67,7 @@ def fail(message, exit_code=1): - """ Print error message and return exit code """ + """Print error message and return exit code""" logging.error(message) print() print("*" * 50) @@ -70,7 +88,7 @@ def fail(message, exit_code=1): def read_file(filename): - """ Read text file and return its contents """ + """Read text file and return its contents""" if not os.path.isfile(filename): fail(f"File not found: {filename}") with open(filename, encoding="utf-8") as handle: @@ -78,19 +96,19 @@ def read_file(filename): def separator(method="info"): - """ Print log separator line to logging.info() or other logging methods """ + """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 """ + """Print stage title into log""" separator(method) getattr(logging, method)("### " + title) separator(method) def valid_parameter(parameter): - """ Check if external parameter is defined and valid """ + """Check if external parameter is defined and valid""" if parameter is None: return False if parameter and parameter[0] == "{": @@ -99,14 +117,14 @@ def valid_parameter(parameter): def log_file_name(): - """ Check and return log file name """ + """Check and return log file name""" if valid_parameter(CODECHECKER_LOG): return CODECHECKER_LOG return None def setup(): - """ Setup logging parameters for execution session """ + """Setup logging parameters for execution session""" if VERBOSITY == "INFO": log_level = logging.INFO elif VERBOSITY == "WARN": @@ -124,7 +142,7 @@ def setup(): def input_data(): - """ Print out input (external) parameters """ + """Print out input (external) parameters""" stage("CodeChecker input data:", "debug") logging.debug("EXECUTION_MODE : %s", str(EXECUTION_MODE)) logging.debug("VERBOSITY : %s", str(VERBOSITY)) @@ -140,7 +158,7 @@ def input_data(): def execute(cmd, env=None, codes=None): - """ Execute CodeChecker commands """ + """Execute CodeChecker commands""" if codes is None: codes = [0] with subprocess.Popen( @@ -162,20 +180,20 @@ def execute(cmd, env=None, codes=None): def create_folder(path): - """ Create folder structure for CodeChecker data files and reports """ + """Create folder structure for CodeChecker data files and reports""" if not os.path.exists(path): os.makedirs(path) def prepare(): - """ Prepare CodeChecker execution environment """ + """Prepare CodeChecker execution environment""" stage("CodeChecker files:") logging.info("Creating folder: %s", CODECHECKER_FILES) create_folder(CODECHECKER_FILES) def analyze(): - """ Run CodeChecker analyze command """ + """Run CodeChecker analyze command""" stage("CodeChecker analyze:") env = os.environ @@ -191,9 +209,11 @@ def analyze(): output = execute(f"{CODECHECKER_PATH} analyzers --details", env=env) logging.debug("Analyzers:\n\n%s", output) - command = f"{CODECHECKER_PATH} analyze --skip={CODECHECKER_SKIPFILE} " \ - f"{COMPILE_COMMANDS} --output={CODECHECKER_FILES}/data " \ - f"--config {CODECHECKER_CONFIG} {CODECHECKER_ANALYZE}" + command = ( + f"{CODECHECKER_PATH} analyze --skip={CODECHECKER_SKIPFILE} " + f"{COMPILE_COMMANDS} --output={CODECHECKER_FILES}/data " + f"--config {CODECHECKER_CONFIG} {CODECHECKER_ANALYZE}" + ) # FIXME: Workaround "CodeChecker simply remove compiler-rt include path". # This can be removed once codechecker 6.16.0 is used. # command += " --keep-gcc-intrin" @@ -206,7 +226,7 @@ def analyze(): def fix_bazel_paths(): - """ Remove Bazel leading paths in all files """ + """Remove Bazel leading paths in all files""" stage("Fix CodeChecker output:") folder = CODECHECKER_FILES logging.info("Fixing Bazel paths in %s", folder) @@ -225,7 +245,7 @@ def fix_bazel_paths(): def realpath(filename): - """ Return real full absolute path for given filename """ + """Return real full absolute path for given filename""" if os.path.exists(filename): real_file_name = os.path.abspath(os.path.realpath(filename)) logging.debug("Updating %s -> %s", filename, real_file_name) @@ -234,7 +254,7 @@ def realpath(filename): def resolve_plist_symlinks(filepath): - """ Resolve the symbolic links in plist files to real file paths """ + """Resolve the symbolic links in plist files to real file paths""" # plistlib replaced readPlist/writePlist with load/dump in Python 3.9. # Since Pylint analyzes every line, # it flags the methods missing in the current environment. @@ -258,7 +278,7 @@ def resolve_plist_symlinks(filepath): def resolve_yaml_symlinks(filepath): - """ Resolve the symbolic links in YAML files to real file paths """ + """Resolve the symbolic links in YAML files to real file paths""" logging.info("Processing YAML file: %s", filepath) fields = [ r"MainSourceFile:\s*", @@ -289,7 +309,7 @@ def resolve_yaml_symlinks(filepath): def resolve_symlinks(): - """ Change ".../execroot/apps" paths to absolute paths in data/* files """ + """Change ".../execroot/apps" paths to absolute paths in data/* files""" stage("Resolve file paths in CodeChecker analyze output:") analyze_outdir = CODECHECKER_FILES + "/data" logging.info( @@ -319,14 +339,18 @@ def update_file_paths(): def parse(): - """ Run CodeChecker parse commands """ + """Run CodeChecker parse commands""" stage("CodeChecker parse:") logging.info("CodeChecker parse -e json") - codechecker_parse = f"{CODECHECKER_PATH} parse --config " \ - f"{CODECHECKER_CONFIG} {CODECHECKER_FILES}/data" + codechecker_parse = ( + f"{CODECHECKER_PATH} parse --config " + f"{CODECHECKER_CONFIG} {CODECHECKER_FILES}/data" + ) # Save results to JSON file - command = f"{codechecker_parse} --export=json > " \ - f"{CODECHECKER_FILES}/result.json" + command = ( + f"{codechecker_parse} --export=json > " + f"{CODECHECKER_FILES}/result.json" + ) execute(command, codes=[0, 2]) # logging.debug( # "JSON:\n\n%s\n", read_file(CODECHECKER_FILES + "/result.json") @@ -350,7 +374,7 @@ def parse(): def run(): - """ Perform all steps for "bazel build" phase """ + """Perform all steps for "bazel build" phase""" prepare() analyze() parse() @@ -358,7 +382,7 @@ def run(): def check_results(): - """ Check/verify CodeChecker results """ + """Check/verify CodeChecker results""" stage("Checking result:") # Get results file and read it result_file = CODECHECKER_FILES + "/result.txt" @@ -405,12 +429,12 @@ def check_results(): def test(): - """ Perform all steps for "bazel test" phase """ + """Perform all steps for "bazel test" phase""" check_results() def main(): - """ Main function """ + """Main function""" setup() input_data() try: diff --git a/src/common.bzl b/src/common.bzl index 198b800e..2a7ca856 100644 --- a/src/common.bzl +++ b/src/common.bzl @@ -50,23 +50,6 @@ def python_toolchain_type(): return "@bazel_tools//tools/python:toolchain_type" return "@rules_python//python:toolchain_type" -def python_path(ctx): - """ - Returns version specific Python path - """ - py_toolchain = ctx.toolchains[python_toolchain_type()] - if hasattr(py_toolchain, "py3_runtime_info"): - py_runtime_info = py_toolchain.py3_runtime_info - python_path = py_runtime_info.interpreter - elif hasattr(py_toolchain, "py3_runtime"): - py_runtime = py_toolchain.py3_runtime - python_path = py_runtime.interpreter_path - else: - fail("The resolved Python toolchain does not provide a Python3 runtime.") - if not python_path: - fail("The resolved Python toolchain does not provide a Python3 interpreter.") - return python_path - def warning(ctx, msg): """ Prints message if the debug tag is enabled. diff --git a/src/per_file.bzl b/src/per_file.bzl index 8a9be0fa..16334136 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -31,6 +31,7 @@ load( def _run_code_checker( ctx, + per_file_script, src, arguments, target, @@ -66,16 +67,22 @@ def _run_code_checker( ";clang-tidy," + clang_tidy_plist.path # Action to run CodeChecker for a file + # env_vars are unused for now, since + # use_default_shell_env and env are incompatible ctx.actions.run( inputs = inputs, outputs = outputs, - executable = ctx.outputs.per_file_script, + executable = per_file_script, arguments = [ + compile_commands_json.path, + " ".join(options), + config_file.path, data_dir, src.path, codechecker_log.path, analyzer_output_paths, ], + tools = [ctx.attr._per_file_script[DefaultInfo].files_to_run], mnemonic = "CodeChecker", use_default_shell_env = True, progress_message = "CodeChecker analyze {}".format(src.short_path), @@ -117,22 +124,6 @@ def _collect_all_sources_and_headers(ctx): all_files += headers return all_files -def _create_wrapper_script(ctx, options, compile_commands_json, config_file): - 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 = { - "{PythonPath}": ctx.attr._python_runtime[PyRuntimeInfo].interpreter_path, - "{compile_commands_json}": compile_commands_json.path, - "{codechecker_args}": options_str, - "{config_file}": config_file.path, - }, - ) - def _per_file_impl(ctx): compile_commands = None for output in compile_commands_impl(ctx): @@ -146,7 +137,13 @@ def _per_file_impl(ctx): options = ctx.attr.default_options + ctx.attr.options all_files = [compile_commands] config_file, env_vars = get_config_file(ctx) - _create_wrapper_script(ctx, options, compile_commands, config_file) + + # Create per_file_script + per_file_script = ctx.actions.declare_file(ctx.label.name + "/per_file_script") + ctx.actions.symlink( + output = per_file_script, + target_file = ctx.executable._per_file_script, + ) for target in ctx.attr.targets: if not CcInfo in target: continue @@ -161,6 +158,7 @@ def _per_file_impl(ctx): args = target[SourceFilesInfo].compilation_db.to_list() outputs = _run_code_checker( ctx, + per_file_script, src, args, target, @@ -221,9 +219,11 @@ per_file_test = rule( default = None, doc = "CodeChecker configuration", ), - "_per_file_script_template": attr.label( - default = ":per_file_script.py", - allow_single_file = True, + "_per_file_script": attr.label( + allow_files = True, + executable = True, + cfg = "target", + default = ":per_file_script", ), "_python_runtime": attr.label( default = "@default_python_tools//:py3_runtime", @@ -232,7 +232,7 @@ per_file_test = rule( outputs = { "compile_commands": "%{name}/compile_commands.json", "test_script": "%{name}/test_script.sh", - "per_file_script": "%{name}/per_file_script.py", + "per_file_script": "%{name}/per_file_script", }, test = True, ) diff --git a/src/per_file_script.py b/src/per_file_script.py index 1cfad4b9..445a0297 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -1,5 +1,3 @@ -#!{PythonPath} - # Copyright 2023 Ericsson AB # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,14 +30,14 @@ # List of pairs of analyzers and their plist files ANALYZER_PLIST_PATHS: Optional[list[list[str]]] = None LOG_FILE: Optional[str] = None -COMPILE_COMMANDS_JSON: str = "{compile_commands_json}" +COMPILE_COMMANDS_JSON: str = sys.argv[1] COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" -CODECHECKER_ARGS: str = "{codechecker_args}" -CONFIG_FILE: str = "{config_file}" -DATA_DIR = sys.argv[1] -FILE_PATH = sys.argv[2] -LOG_FILE = sys.argv[3] -ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[4].split(";")] +CODECHECKER_ARGS: str = sys.argv[2] +CONFIG_FILE: str = sys.argv[3] +DATA_DIR = sys.argv[4] +FILE_PATH = sys.argv[5] +LOG_FILE = sys.argv[6] +ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[7].split(";")] def log(msg: str) -> None: @@ -147,7 +145,7 @@ def main(): """ Main function of CodeChecker wrapper """ - if len(sys.argv) != 5: + if len(sys.argv) != 8: print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths()