diff --git a/src/per_file.bzl b/src/per_file.bzl index ae19582a..99ddfa75 100644 --- a/src/per_file.bzl +++ b/src/per_file.bzl @@ -17,6 +17,13 @@ Rulesets for running codechecker in a different Bazel action for each translation unit. """ +load( + "compile_commands.bzl", + "compile_commands_aspect", + "compile_commands_impl", + "platforms_transition", + "SourceFilesInfo", +) load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES") load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") load("codechecker_config.bzl", "get_config_file") @@ -26,6 +33,7 @@ def _run_code_checker( ctx, src, arguments, + target, label, options, config_file, @@ -49,14 +57,13 @@ def _run_code_checker( inputs = [compile_commands_json, config_file] + sources_and_headers else: # NOTE: we collect only headers, so CTU may not work! - headers = depset([src], transitive = [compilation_context.headers]) + headers = depset(transitive = target[SourceFilesInfo].headers.to_list()) inputs = depset([compile_commands_json, config_file, src], transitive = [headers]) outputs = [clang_tidy_plist, clangsa_plist, codechecker_log] analyzer_output_paths = "clangsa," + clangsa_plist.path + \ ";clang-tidy," + clang_tidy_plist.path - # Action to run CodeChecker for a file ctx.actions.run( inputs = inputs, @@ -92,190 +99,22 @@ def check_valid_file_type(src): return True return False -def _rule_sources(ctx): - srcs = [] - if hasattr(ctx.rule.attr, "srcs"): - for src in ctx.rule.attr.srcs: - srcs += [src for src in src.files.to_list() if check_valid_file_type(src)] - return srcs - -def _toolchain_flags(ctx, action_name = ACTION_NAMES.cpp_compile): - cc_toolchain = find_cpp_toolchain(ctx) - feature_configuration = cc_common.configure_features( - ctx = ctx, - cc_toolchain = cc_toolchain, - ) - user_comp_flag_builder = ctx.fragments.cpp.copts - if action_name == ACTION_NAMES.cpp_compile: - user_comp_flag_builder += ctx.fragments.cpp.cxxopts - elif action_name == ACTION_NAMES.c_compile: - user_comp_flag_builder += ctx.fragments.cpp.conlyopts - else: - fail("Unhandled action name!") - compile_variables = cc_common.create_compile_variables( - feature_configuration = feature_configuration, - cc_toolchain = cc_toolchain, - user_compile_flags = user_comp_flag_builder, - ) - flags = cc_common.get_memory_inefficient_command_line( - feature_configuration = feature_configuration, - action_name = action_name, - variables = compile_variables, - ) - compiler = cc_common.get_tool_for_action( - feature_configuration = feature_configuration, - action_name = action_name, - ) - return [compiler] + flags - -def _compile_args(compilation_context): - compile_args = [] - for define in compilation_context.defines.to_list(): - compile_args.append("-D" + define) - for define in compilation_context.local_defines.to_list(): - compile_args.append("-D" + define) - for include in compilation_context.framework_includes.to_list(): - compile_args.append("-F" + include) - for include in compilation_context.includes.to_list(): - compile_args.append("-I" + include) - for include in compilation_context.quote_includes.to_list(): - compile_args.append("-iquote " + include) - for include in compilation_context.system_includes.to_list(): - compile_args.append("-isystem " + include) - return compile_args - -def _safe_flags(flags): - # Some flags might be used by GCC, but not understood by Clang. - # Remove them here, to allow users to run clang-tidy, without having - # a clang toolchain configured (that would produce a good command line with --compiler clang) - unsupported_flags = [ - "-fno-canonical-system-headers", - "-fstack-usage", - ] - - return [flag for flag in flags if flag not in unsupported_flags] - -CompileInfo = provider( - doc = "Source files and corresponding compilation arguments", - fields = { - "arguments": "dict: file -> list of arguments", - }, -) - -def _compile_info_sources(deps): - sources = [] - if type(deps) == "list": - for dep in deps: - if CompileInfo in dep: - if hasattr(dep[CompileInfo], "arguments"): - srcs = dep[CompileInfo].arguments.keys() - sources += srcs - return sources - -def _collect_all_sources(ctx): - sources = _rule_sources(ctx) - for attr in SOURCE_ATTR: - if hasattr(ctx.rule.attr, attr): - deps = getattr(ctx.rule.attr, attr) - sources += _compile_info_sources(deps) - - # Remove duplicates - sources = depset(sources).to_list() - return sources - -def _compile_info_aspect_impl(target, ctx): - if not CcInfo in target: - return [] - compilation_context = target[CcInfo].compilation_context - - rule_flags = ctx.rule.attr.copts if hasattr(ctx.rule.attr, "copts") else [] - c_flags = _safe_flags(_toolchain_flags(ctx, ACTION_NAMES.c_compile) + rule_flags) # + ["-xc"] - cxx_flags = _safe_flags(_toolchain_flags(ctx, ACTION_NAMES.cpp_compile) + rule_flags) # + ["-xc++"] - - srcs = _collect_all_sources(ctx) - - compile_args = _compile_args(compilation_context) - arguments = {} - for src in srcs: - if src.extension.lower() in ["c"]: - flags = c_flags - elif src.extension.lower() in ["cc", "cpp", "cxx", "c++"]: - flags = cxx_flags - else: - print("Unknown file extension for", src.short_path, "defaulting to C++ compile flags") - flags = cxx_flags - arguments[src] = flags + compile_args + [src.path] - return [ - CompileInfo( - arguments = arguments, - ), - ] - -compile_info_aspect = aspect( - implementation = _compile_info_aspect_impl, - fragments = ["cpp"], - attrs = { - "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")), - }, - attr_aspects = SOURCE_ATTR, - toolchains = ["@bazel_tools//tools/cpp:toolchain_type"], -) - -def _compile_commands_json(compile_commands): - json_file = "[\n" - entries = [json.encode(entry) for entry in compile_commands] - json_file += ",\n".join(entries) - json_file += "]\n" - return json_file - -def _compile_commands_data(ctx): - compile_commands = [] - for target in ctx.attr.targets: - if not CcInfo in target: - continue - if CompileInfo in target: - if hasattr(target[CompileInfo], "arguments"): - srcs = target[CompileInfo].arguments.keys() - for src in srcs: - args = target[CompileInfo].arguments[src] - - # print("args =", str(args)) - record = struct( - file = src.path, - command = " ".join(args), - directory = ".", - ) - compile_commands.append(record) - return compile_commands - -def _compile_commands_impl(ctx): - compile_commands = _compile_commands_data(ctx) - content = _compile_commands_json(compile_commands) - file_name = ctx.attr.name + "/data/compile_commands.json" - compile_commands_json = ctx.actions.declare_file(file_name) - ctx.actions.write( - output = compile_commands_json, - content = content, - ) - return compile_commands_json - def _collect_all_sources_and_headers(ctx): # NOTE: we are only using this function for CTU all_files = [] - headers = depset() for target in ctx.attr.targets: if not CcInfo in target: continue - if CompileInfo in target: - if hasattr(target[CompileInfo], "arguments"): - srcs = target[CompileInfo].arguments.keys() - all_files += srcs - compilation_context = target[CcInfo].compilation_context + if SourceFilesInfo in target: + if (hasattr(target[SourceFilesInfo], "transitive_source_files") + and hasattr(target[SourceFilesInfo], "headers")): + srcs = target[SourceFilesInfo].transitive_source_files.to_list() headers = depset( - transitive = [headers, compilation_context.headers], - ) - sources_and_headers = all_files + headers.to_list() - return sources_and_headers + transitive = target[SourceFilesInfo].headers.to_list() + ).to_list() + all_files += srcs + all_files += headers + return all_files def _create_wrapper_script(ctx, options, compile_commands_json, config_file): options_str = "" @@ -294,31 +133,41 @@ def _create_wrapper_script(ctx, options, compile_commands_json, config_file): ) def _per_file_impl(ctx): - compile_commands_json = _compile_commands_impl(ctx) + compile_commands = None + for output in compile_commands_impl(ctx): + if type(output) == "DefaultInfo": + compile_commands = output.files.to_list()[0] + if not compile_commands: + fail("Failed to generate compile_commands.json file!") + if compile_commands != ctx.outputs.compile_commands: + fail("Seems compile_commands.json file is incorrect!") sources_and_headers = _collect_all_sources_and_headers(ctx) options = ctx.attr.default_options + ctx.attr.options - all_files = [compile_commands_json] + all_files = [compile_commands] config_file, env_vars = get_config_file(ctx) - _create_wrapper_script(ctx, options, compile_commands_json, config_file) + _create_wrapper_script(ctx, options, compile_commands, config_file) for target in ctx.attr.targets: if not CcInfo in target: continue - if CompileInfo in target: - if hasattr(target[CompileInfo], "arguments"): - srcs = target[CompileInfo].arguments.keys() + if SourceFilesInfo in target: + if hasattr(target[SourceFilesInfo], "transitive_source_files"): + srcs = target[SourceFilesInfo].transitive_source_files.to_list() all_files += srcs compilation_context = target[CcInfo].compilation_context for src in srcs: - args = target[CompileInfo].arguments[src] + if not check_valid_file_type(src): + continue + args = target[SourceFilesInfo].compilation_db.to_list() outputs = _run_code_checker( ctx, src, args, + target, ctx.attr.name, options, config_file, env_vars, - compile_commands_json, + compile_commands, compilation_context, sources_and_headers, ) @@ -363,7 +212,7 @@ per_file_test = rule( ), "targets": attr.label_list( aspects = [ - compile_info_aspect, + compile_commands_aspect, ], doc = "List of compilable targets which should be checked.", ), @@ -381,6 +230,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", }, diff --git a/test/unit/compile_flags/test_compile_flags.py b/test/unit/compile_flags/test_compile_flags.py index 84d89c07..e72778d7 100644 --- a/test/unit/compile_flags/test_compile_flags.py +++ b/test/unit/compile_flags/test_compile_flags.py @@ -85,7 +85,6 @@ def test_bazel_test_per_file_filter(self): compile_commands = os.path.join( self.BAZEL_BIN_DIR, "per_file_filter", - "data", "compile_commands.json", ) self.assertTrue(os.path.exists(compile_commands)) diff --git a/test/unit/external_repository/test_external_repo.py b/test/unit/external_repository/test_external_repo.py index 0028bd3c..d9d0791e 100644 --- a/test/unit/external_repository/test_external_repo.py +++ b/test/unit/external_repository/test_external_repo.py @@ -110,8 +110,7 @@ def test_per_file_external_lib(self): """Test: bazel build :per_file_external_deps""" ret, _, _ = self.run_command( "bazel build :per_file_external_deps") - # TODO: set to 1, the nothing header should be found - self.assertEqual(ret, 1) + self.assertEqual(ret, 0) if __name__ == "__main__": diff --git a/test/unit/virtual_include/test_virtual_include.py b/test/unit/virtual_include/test_virtual_include.py index a0782b11..0dc53462 100644 --- a/test/unit/virtual_include/test_virtual_include.py +++ b/test/unit/virtual_include/test_virtual_include.py @@ -120,8 +120,7 @@ def test_bazel_per_file_implementation_deps_virtual_include(self): ret, _, _ = self.run_command( "bazel build --experimental_cc_implementation_deps //test/unit/virtual_include:per_file_impl_deps_include" ) - # TODO: change to 0, CodeChecker should finish analysis successfully - self.assertEqual(ret, 1) + self.assertEqual(ret, 0) if __name__ == "__main__":