diff --git a/MODULE.bazel b/MODULE.bazel index fde5f260..0ddbaece 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -38,3 +38,7 @@ codechecker_extension = use_extension( "module_register_default_codechecker_tools", ) use_repo(codechecker_extension, "default_codechecker_tools") + +register_toolchains( + "//src:codechecker_local_toolchain", +) diff --git a/README.md b/README.md index b8d5e702..883596bd 100644 --- a/README.md +++ b/README.md @@ -427,3 +427,83 @@ After that you can find all artifacts in `bazel-bin` directory: # compile_commands.json for compile_commands_pass cat bazel-bin/test/compile_commands_pass/compile_commands.json + +Toolchains +---------- + +The previous rules locate the CodeChecker, Clang and clang-tidy executables through a +Bazel [toolchain](https://bazel.build/extending/toolchains). Modeling the tools +as a toolchain means the analysis rule (`codechecker_test`) never hardcode binary paths: +Bazel resolves the correct tools at build time, and you can override them +per-project or per-platform without touching the rules themselves. + +### The default toolchain + +For most users no setup is required. `rules_codechecker` ships with a module +extension that provisions a default set of tools and a pre-registered +toolchain. + +> [!NOTE] +> The default tools resolve to the `CodeChecker`, `clang` and `clang-tidy` +> binaries available from PATH on your system (see the [Prerequisites](#prerequisites)). + +To enable it, add the following to your `MODULE.bazel`: + +```python +codechecker_extension = use_extension( + "@rules_codechecker//src:tools.bzl", + "module_register_default_codechecker_tools", +) +use_repo(codechecker_extension, "default_codechecker_tools") +register_toolchains("@rules_codechecker//src:codechecker_local_toolchain") +``` + +Or the following to your `WORKSPACE`: + +```python +load( + "@rules_codechecker//src:tools.bzl", + "register_default_codechecker_tools", +) + +register_default_codechecker_tools() +register_toolchains( + "//src:codechecker_local_toolchain", +) +``` + +### Providing your own tools + +If you don't want the default system tools -- for example to pin a specific +CodeChecker version or point at a custom build —- you can define and register your own toolchain. + +First, define an implementation in a BUILD file with `codechecker_toolchain()`: + +```python +load( + "@rules_codechecker//src:codechecker_toolchain.bzl", + "codechecker_toolchain", +) + +codechecker_toolchain( + name = "codechecker_custom", + clang_tidy_binary = "@my_tools//:clang-tidy", + clangsa_binary = "@my_tools//:clang", + codechecker_binary = "@my_tools//:CodeChecker", +) + +toolchain( + name = "codechecker_custom_toolchain", + toolchain = ":codechecker_custom", + toolchain_type = "@rules_codechecker//src:toolchain_type", + # Optionally constrain which execution platform this applies to: + # exec_compatible_with = ["@platforms//os:linux"], +) +``` +Finally register it in your `MODULE.bazel` (or `WORKSPACE`): +```python +register_toolchains("//path/to:codechecker_custom_toolchain") +``` + +[!NOTE] +> Toolchains you register yourself take precedence over the default one if registered earlier. diff --git a/WORKSPACE b/WORKSPACE index 6be16fee..d03a850f 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -22,7 +22,10 @@ load( register_default_python_toolchain() -register_toolchains("@default_python_tools//:python_toolchain") +register_toolchains( + "@default_python_tools//:python_toolchain", + "//src:codechecker_local_toolchain", +) register_default_codechecker_tools() diff --git a/src/BUILD b/src/BUILD index 79c101ee..e79c6332 100644 --- a/src/BUILD +++ b/src/BUILD @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +load(":codechecker_toolchain.bzl", "codechecker_toolchain") # Tool filter compile_commands.json file # In bazel 6 we use our own python toolchain, @@ -63,3 +64,20 @@ label_flag( build_setting_default = ":clang_tidy_additional_deps_default", visibility = ["//visibility:public"], ) + +# named this by convention +# https://bazel.build/extending/toolchains#:~:text=%23%20By%20convention%2C%20toolchain%5Ftype%20targets%20are%20named%20%22toolchain%5Ftype%22%20and +toolchain_type(name = "toolchain_type") + +codechecker_toolchain( + name = "codechecker_local", + clang_tidy_binary = "@default_codechecker_tools//:clang_tidy", + clangsa_binary = "@default_codechecker_tools//:clang", + codechecker_binary = "@default_codechecker_tools//:CodeChecker", +) + +toolchain( + name = "codechecker_local_toolchain", + toolchain = "codechecker_local", + toolchain_type = ":toolchain_type", +) diff --git a/src/codechecker.bzl b/src/codechecker.bzl index 899ad917..8a1f5323 100644 --- a/src/codechecker.bzl +++ b/src/codechecker.bzl @@ -16,12 +16,6 @@ Rulesets for running codechecker in a single Bazel job. """ -load( - "@default_codechecker_tools//:defs.bzl", - "CLANG_BIN_PATH", - "CLANG_TIDY_BIN_PATH", - "CODECHECKER_BIN_PATH", -) load( "codechecker_config.bzl", "codechecker_config_internal", @@ -99,6 +93,8 @@ def _codechecker_impl(ctx): config_file, codechecker_env = get_config_file(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, @@ -107,10 +103,10 @@ def _codechecker_impl(ctx): substitutions = { "{Mode}": "Run", "{Verbosity}": "DEBUG", - "{clang_bin}": CLANG_BIN_PATH, - "{clang_tidy_bin}": CLANG_TIDY_BIN_PATH, + "{clang_bin}": info.clangsa_bin.path, + "{clang_tidy_bin}": info.clang_tidy_bin.path, "{codechecker_analyze}": " ".join(ctx.attr.analyze), - "{codechecker_bin}": CODECHECKER_BIN_PATH, + "{codechecker_bin}": info.codechecker_bin.path, "{codechecker_config}": config_file.path, "{codechecker_env}": codechecker_env, "{codechecker_files}": codechecker_files.path, @@ -123,6 +119,9 @@ def _codechecker_impl(ctx): ctx.actions.run( inputs = depset( [ + info.codechecker_bin, + info.clangsa_bin, + info.clang_tidy_bin, ctx.outputs.codechecker_script, ctx.outputs.codechecker_commands, ctx.outputs.codechecker_skipfile, @@ -209,7 +208,10 @@ codechecker = rule( "codechecker_skipfile": "%{name}/codechecker_skipfile.cfg", "compile_commands": "%{name}/compile_commands.json", }, - toolchains = [python_toolchain_type()], + toolchains = [ + python_toolchain_type(), + "//src:toolchain_type", + ], ) def _codechecker_test_impl(ctx): @@ -229,6 +231,8 @@ def _codechecker_test_impl(ctx): if not codechecker_files: fail("Execution results required for codechecker test are not available") + info = ctx.toolchains["//src:toolchain_type"].codecheckerinfo + # Create test script from template ctx.actions.expand_template( template = ctx.file._codechecker_script_template, @@ -238,15 +242,20 @@ def _codechecker_test_impl(ctx): "{Mode}": "Test", "{Severities}": " ".join(ctx.attr.severities), "{Verbosity}": "INFO", - "{clang_bin}": CLANG_BIN_PATH, - "{clang_tidy_bin}": CLANG_TIDY_BIN_PATH, - "{codechecker_bin}": CODECHECKER_BIN_PATH, + "{clang_bin}": info.clangsa_bin.short_path, + "{clang_tidy_bin}": info.clang_tidy_bin.short_path, + "{codechecker_bin}": info.codechecker_bin.short_path, "{codechecker_files}": codechecker_files.short_path, }, ) # 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, + info.codechecker_bin, + info.clang_tidy_bin, + info.clangsa_bin, + ] return [ DefaultInfo( files = depset(all_files), @@ -306,7 +315,10 @@ _codechecker_test = rule( "codechecker_test_script": "%{name}/codechecker_test_script.py", "compile_commands": "%{name}/compile_commands.json", }, - toolchains = [python_toolchain_type()], + toolchains = [ + python_toolchain_type(), + "//src:toolchain_type", + ], test = True, ) diff --git a/src/codechecker_script.py b/src/codechecker_script.py index 5854ec2d..51f22eaa 100644 --- a/src/codechecker_script.py +++ b/src/codechecker_script.py @@ -29,7 +29,7 @@ EXECUTION_MODE = "{Mode}" VERBOSITY = "{Verbosity}" -CODECHECKER_PATH = "{codechecker_bin}" +CODECHECKER_PATH = os.path.realpath("{codechecker_bin}") CLANG_PATH = "{clang_bin}" CLANG_TIDY_PATH = "{clang_tidy_bin}" CODECHECKER_SKIPFILE = "{codechecker_skipfile}" diff --git a/src/codechecker_toolchain.bzl b/src/codechecker_toolchain.bzl new file mode 100644 index 00000000..2d382350 --- /dev/null +++ b/src/codechecker_toolchain.bzl @@ -0,0 +1,46 @@ +""" +This file provides the toolchain rule for CodeChecker +""" + +CodeCheckerInfo = provider( + doc = "This provider provides the executable path for CodeChecker and its related tools", + fields = { + "clang_tidy_bin": "clang-tidy executable", + "clangsa_bin": "Clang executable", + "codechecker_bin": "CodeChecker executable", + }, +) + +def _codechecker_toolchain_impl(ctx): + toolchain_info = platform_common.ToolchainInfo( + codecheckerinfo = CodeCheckerInfo( + codechecker_bin = ctx.executable.codechecker_binary, + clang_tidy_bin = ctx.executable.clang_tidy_binary, + clangsa_bin = ctx.executable.clangsa_binary, + ), + ) + return [toolchain_info] + +codechecker_toolchain = rule( + implementation = _codechecker_toolchain_impl, + attrs = { + "clang_tidy_binary": attr.label( + doc = "clang-tidy executable", + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "clangsa_binary": attr.label( + doc = "clang executable", + allow_single_file = True, + executable = True, + cfg = "exec", + ), + "codechecker_binary": attr.label( + doc = "CodeChecker executable", + allow_single_file = True, + executable = True, + cfg = "exec", + ), + }, +) diff --git a/src/per_file.bzl b/src/per_file.bzl index 57d55e56..d69d9645 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -59,11 +59,16 @@ def _run_code_checker( content = "\n".join(ctx.attr.skip), ) + info = ctx.toolchains["//src:toolchain_type"].codecheckerinfo + if "--ctu" in options: inputs = [ compile_commands_json, config_file, config, + info.codechecker_bin, + info.clangsa_bin, + info.clang_tidy_bin, ] + sources_and_headers else: # NOTE: we collect only headers, so CTU may not work! @@ -73,6 +78,9 @@ def _run_code_checker( config_file, src, config, + info.codechecker_bin, + info.clangsa_bin, + info.clang_tidy_bin, ], transitive = [headers]) outputs = [clang_tidy_plist, clangsa_plist, codechecker_log] @@ -80,17 +88,22 @@ def _run_code_checker( analyzer_output_paths = "clangsa," + clangsa_plist.path + \ ";clang-tidy," + clang_tidy_plist.path + analyzer_executables = "clangsa:" + info.clangsa_bin.path + \ + ";clang-tidy:" + info.clang_tidy_bin.path + # Action to run CodeChecker for a file ctx.actions.run( inputs = inputs, outputs = outputs, executable = ctx.outputs.per_file_script, arguments = [ + info.codechecker_bin.path, data_dir, src.path, codechecker_log.path, config.path, analyzer_output_paths, + analyzer_executables, ], mnemonic = "CodeChecker", use_default_shell_env = True, @@ -258,4 +271,5 @@ per_file_test = rule( "test_script": "%{name}/test_script.sh", }, test = True, + toolchains = ["//src:toolchain_type"], ) diff --git a/src/per_file_script.py b/src/per_file_script.py index ab9901dc..dc5a1476 100644 --- a/src/per_file_script.py +++ b/src/per_file_script.py @@ -28,14 +28,22 @@ COMPILE_COMMANDS_ABSOLUTE: str = f"{COMPILE_COMMANDS_JSON}.abs" CODECHECKER_ARGS: str = "{codechecker_args}" CONFIG_FILE: str = "{config_file}" -SKIP_FILE: str = sys.argv[4] +SKIP_FILE: str = sys.argv[5] +CODECHECKER_BIN = os.path.realpath(sys.argv[1]) # The output directory for CodeChecker -DATA_DIR = sys.argv[1] +DATA_DIR = sys.argv[2] # The file to be analyzed -FILE_PATH = sys.argv[2] -LOG_FILE = sys.argv[3] +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[5].split(";")] +ANALYZER_PLIST_PATHS = [item.split(",") for item in sys.argv[6].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 + ] +) + EMPTY_PLIST = """ @@ -81,12 +89,22 @@ def _create_compile_commands_json_with_absolute_paths(): new_file.write(new_content) +def _get_codechecker_env() -> dict[str, str]: + """ + Returns the environment for running CodeChecker + """ + cc_env = os.environ.copy() + # Overwrite analyzer paths + cc_env["CC_ANALYZER_BIN"] = ANALYZER_EXECUTABLES_ENV_VAR + return cc_env + + def _run_codechecker() -> None: """ Runs CodeChecker analyze """ codechecker_cmd: list[str] = ( - ["CodeChecker", "analyze"] + [CODECHECKER_BIN, "analyze"] + CODECHECKER_ARGS.split() + ["--output=" + DATA_DIR] + ["--file=*/" + FILE_PATH] @@ -102,7 +120,7 @@ def _run_codechecker() -> None: result = subprocess.run( ["echo", "$PATH"], shell=True, - env=os.environ, + env=_get_codechecker_env(), capture_output=True, text=True, check=False, @@ -113,7 +131,7 @@ def _run_codechecker() -> None: with open(LOG_FILE, "a", encoding="utf-8") as log_file: subprocess.run( codechecker_cmd, - env=os.environ, + env=_get_codechecker_env(), stdout=log_file, stderr=log_file, check=True, @@ -173,7 +191,7 @@ def main(): """ Main function of CodeChecker wrapper """ - if len(sys.argv) != 6: + if len(sys.argv) != 8: print("Wrong amount of arguments") sys.exit(1) _create_compile_commands_json_with_absolute_paths() diff --git a/src/tools.bzl b/src/tools.bzl index f15d4fd1..4ef0f90b 100644 --- a/src/tools.bzl +++ b/src/tools.bzl @@ -97,12 +97,6 @@ module_register_default_python_toolchain = module_extension( ) def _codechecker_local_repository_impl(repository_ctx): - repository_ctx.file( - repository_ctx.path("BUILD"), - content = "", - executable = False, - ) - codechecker_bin_path = repository_ctx.which("CodeChecker") if not codechecker_bin_path: fail("ERROR! CodeChecker is not detected") @@ -123,6 +117,32 @@ def _codechecker_local_repository_impl(repository_ctx): executable = False, ) + repository_ctx.symlink(codechecker_bin_path, "codechecker_bin") + repository_ctx.symlink(clang_bin_path, "clang_bin") + repository_ctx.symlink(clang_tidy_bin_path, "clang_tidy_bin") + + repository_ctx.file( + repository_ctx.path("BUILD"), + content = """ +filegroup( + name = "CodeChecker", + srcs = ["codechecker_bin"], + visibility = ["//visibility:public"], +) +filegroup( + name = "clang", + srcs = ["clang_bin"], + visibility = ["//visibility:public"], +) +filegroup( + name = "clang_tidy", + srcs = ["clang_tidy_bin"], + visibility = ["//visibility:public"], +) + """, + executable = False, + ) + default_codechecker_tools = repository_rule( attrs = {}, local = True, diff --git a/test/foss/templates/WORKSPACE.template b/test/foss/templates/WORKSPACE.template index 82d71ce8..e6ffe6a0 100644 --- a/test/foss/templates/WORKSPACE.template +++ b/test/foss/templates/WORKSPACE.template @@ -14,7 +14,10 @@ load( ) register_default_python_toolchain() -register_toolchains("@default_python_tools//:python_toolchain") +register_toolchains( + "@default_python_tools//:python_toolchain", + "@rules_codechecker//src:codechecker_local_toolchain", +) register_default_codechecker_tools()