From 0216fb1e682c52a9f09b7a3b35743c3a1567fb4b Mon Sep 17 00:00:00 2001 From: Daniel Moody Date: Thu, 19 Mar 2026 15:07:57 -0500 Subject: [PATCH] SERVER-122044 fix clang-tidy ide setup and coverity run (#49986) GitOrigin-RevId: 89fe63d799c851bde830c5b9d35bc34a81f25cde --- bazel/wrapper_hook/compiledb.py | 147 +++++++++++++++- bazel/wrapper_hook/plus_interface.py | 27 +++ buildscripts/setup_clang_tidy.py | 25 +-- .../tests/test_bazel_plus_test_interface.py | 51 ++++++ .../tests/test_compiledb_output_format.py | 32 +++- buildscripts/tests/test_compiledb_posthook.py | 162 ++++++++++++++++++ evergreen/coverity_build.sh | 3 + evergreen/validate_compile_commands.py | 55 ++++++ evergreen/validate_compile_commands_test.py | 54 ++++++ 9 files changed, 524 insertions(+), 32 deletions(-) create mode 100644 buildscripts/tests/test_compiledb_posthook.py diff --git a/bazel/wrapper_hook/compiledb.py b/bazel/wrapper_hook/compiledb.py index 63b32150b10..aa0b2b6fd69 100644 --- a/bazel/wrapper_hook/compiledb.py +++ b/bazel/wrapper_hook/compiledb.py @@ -99,16 +99,100 @@ def _format_elapsed(reference_time): return f"{int(hours)}h {int(minutes)}m {seconds:.1f}s" -def _log_progress(message): - line = f"[compiledb +{_format_elapsed(COMPILEDB_START_TIME)}] {message}" +def _get_output_stream(): for stream in [sys.stdout, sys.stderr, sys.__stderr__]: if not stream: continue try: - print(line, file=stream, flush=True) - return + if not stream.writable(): + continue + return stream except (ValueError, OSError): continue + return None + + +def _log_progress(message): + line = f"[compiledb +{_format_elapsed(COMPILEDB_START_TIME)}] {message}" + stream = _get_output_stream() + if stream: + try: + print(line, file=stream, flush=True) + except (ValueError, OSError): + pass + + +def _run_build_command(cmd): + """Run a bazel build, streaming output directly to the terminal. + + Uses a PTY so bazel sees a real terminal (colors, progress bar + overwrites). Both stdout and stderr are routed through the PTY + and echoed to the wrapper's output stream so the output is visible + even when tools/bazel has redirected the default fds to a log file. + + Falls back to a plain subprocess when no output stream is available + or the ``pty`` module is missing (e.g. Windows). + """ + + stream = _get_output_stream() + out_fd = None + if stream: + try: + out_fd = stream.fileno() + except (AttributeError, OSError): + pass + + if out_fd is None: + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + if proc.returncode != 0: + raise RuntimeError( + f"Command failed (rc={proc.returncode}): {' '.join(cmd)}\n" + f"--- output ---\n{proc.stdout.decode(errors='replace')}" + ) + return + + try: + import pty + + parent_fd, child_fd = pty.openpty() + proc = subprocess.Popen(cmd, stdout=child_fd, stderr=child_fd, stdin=child_fd) + os.close(child_fd) + captured = b"" + try: + while True: + try: + data = os.read(parent_fd, 4096) + except OSError as e: + if e.errno != errno.EIO: + raise + break + if not data: + break + captured += data + try: + os.write(out_fd, data) + except OSError: + pass + finally: + os.close(parent_fd) + returncode = proc.wait() + stdout = captured.decode(errors="replace") + except ModuleNotFoundError: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + captured = b"" + for chunk in iter(lambda: proc.stdout.read(4096), b""): + captured += chunk + try: + os.write(out_fd, chunk) + except OSError: + pass + returncode = proc.wait() + stdout = captured.decode(errors="replace") + + if returncode != 0: + raise RuntimeError( + f"Command failed (rc={returncode}): {' '.join(cmd)}\n" f"--- output ---\n{stdout}" + ) def clear_compiledb_posthook_state(): @@ -205,6 +289,15 @@ def _resolve_compiledb_targets(target_scope_override=None, requested_targets=Non return default_target_scope, build_targets, target_scope_expr +def _resolve_extra_build_targets(extra_build_targets=None, setup_clang_tidy=False): + resolved_targets = list(extra_build_targets or []) + if setup_clang_tidy: + for target in SETUP_CLANG_TIDY_BUILD_TARGETS: + if target not in resolved_targets: + resolved_targets.append(target) + return resolved_targets + + def _resolve_compiledb_flags(compiledb_config, requested_build_flags=None): build_flags = list(requested_build_flags or []) @@ -523,10 +616,14 @@ def _collect_aspect_fragment_paths( compiledb_bazelrc, compiledb_config, target_scope_expr, + forwarded_startup_args=None, ): query_cmd = ( [bazel_bin] - + ([f"--output_base={output_base}"] if persistent_compdb else []) + + ( + forwarded_startup_args + or ([f"--output_base={output_base}"] if persistent_compdb else []) + ) + compiledb_bazelrc + ["aquery"] + ([f"--symlink_prefix={symlink_prefix}"] if persistent_compdb else []) @@ -745,7 +842,10 @@ def _generate_compiledb_via_aspect( target_scope_override=target_scope_override, requested_targets=requested_targets, ) - extra_build_targets = list(extra_build_targets or []) + extra_build_targets = _resolve_extra_build_targets( + extra_build_targets=extra_build_targets, + setup_clang_tidy=setup_clang_tidy, + ) build_flags = _resolve_compiledb_flags( compiledb_config, requested_build_flags=requested_build_flags, @@ -756,13 +856,25 @@ def _generate_compiledb_via_aspect( with tempfile.NamedTemporaryFile(delete=False) as buildevents: buildevents_path = buildevents.name + # Build the startup args for the compiledb build invocation. + # When persistent_compdb, we use a dedicated output_base (overrides any + # --output_user_root in the original startup_args, which is fine). + # Otherwise, forward the caller's startup_args so the build resolves to + # the same output tree (e.g. CI passes --output_user_root). + forwarded_startup_args = list(startup_args or []) + if persistent_compdb: + forwarded_startup_args = [ + arg for arg in forwarded_startup_args if not arg.startswith("--output_base=") + ] + forwarded_startup_args.append(f"--output_base={output_base}") + try: if not skip_build: build_start = time.monotonic() _log_progress("Generating compiledb command fragments via aspect...") build_cmd = ( [bazel_bin] - + ([f"--output_base={output_base}"] if persistent_compdb else []) + + forwarded_startup_args + compiledb_bazelrc + ["build"] + ([f"--symlink_prefix={symlink_prefix}"] if persistent_compdb else []) @@ -771,9 +883,8 @@ def _generate_compiledb_via_aspect( f"--build_event_json_file={buildevents_path}", ] + build_targets - + extra_build_targets ) - run_pty_command(build_cmd) + _run_build_command(build_cmd) _log_progress( "Generated compiledb command fragments via aspect " f"in {_format_elapsed(build_start)}" @@ -795,6 +906,7 @@ def _generate_compiledb_via_aspect( compiledb_bazelrc=compiledb_bazelrc, compiledb_config=build_flags, target_scope_expr=target_scope_expr, + forwarded_startup_args=forwarded_startup_args, ) raw_entries = load_compile_command_fragments_from_paths(fragment_paths) if not raw_entries: @@ -825,6 +937,23 @@ def _generate_compiledb_via_aspect( output_json.sort(key=compile_command_sort_key) write_compile_commands(output_json, REPO_ROOT / "compile_commands.json") + if setup_clang_tidy and extra_build_targets: + _log_progress("Building clang-tidy IDE targets...") + tidy_build_cmd = ( + [bazel_bin] + + forwarded_startup_args + + compiledb_bazelrc + + ["build"] + + ([f"--symlink_prefix={symlink_prefix}"] if persistent_compdb else []) + + extra_build_targets + ) + try: + _run_build_command(tidy_build_cmd) + except RuntimeError: + _log_progress( + "Warning: failed to build clang-tidy targets; " "skipping clang-tidy IDE setup." + ) + setup_clang_tidy = False if setup_clang_tidy: setup_clang_tidy_from_built_outputs() finally: diff --git a/bazel/wrapper_hook/plus_interface.py b/bazel/wrapper_hook/plus_interface.py index 9063c745dc7..d2d01575103 100644 --- a/bazel/wrapper_hook/plus_interface.py +++ b/bazel/wrapper_hook/plus_interface.py @@ -15,6 +15,7 @@ sys.path.append(str(REPO_ROOT)) from bazel.wrapper_hook.compiledb import ( clear_compiledb_posthook_state, generate_compiledb, + prepare_compiledb_posthook_args, ) from bazel.wrapper_hook.lint import run_rules_lint from bazel.wrapper_hook.wrapper_debug import wrapper_debug @@ -163,6 +164,7 @@ def test_runner_interface( plus_starts = ("+", ":+", "//:+") skip_plus_interface = True compiledb_target = False + compiledb_config = False setup_clang_tidy = False clang_tidy = False lint_target = False @@ -235,6 +237,8 @@ def test_runner_interface( val = config_value if val in {"opt", "dbg", "fastbuild", "dbg_aubsan", "dbg_tsan"}: config_mode = val + if val in ("compiledb", "compiledb-aspect"): + compiledb_config = True if val == "clang-tidy": clang_tidy = True if arg.startswith(plus_starts): @@ -309,6 +313,29 @@ def test_runner_interface( + ["--", "ALL_PASSING"] ) + if compiledb_config and not compiledb_target and current_bazel_command == "build": + parsed_build_flags, parsed_build_targets, parsed_target_pattern_file = ( + _parse_targets_and_flags(args[command_index + 1 :], {}, [], []) + ) + + posthook_targets = list(parsed_build_targets) + if parsed_target_pattern_file and os.path.isfile(parsed_target_pattern_file): + with open(parsed_target_pattern_file, "r", encoding="utf-8") as f: + posthook_targets.extend(line.strip() for line in f if line.strip()) + + return prepare_compiledb_posthook_args( + bazel_bin=args[0], + startup_args=startup_args, + command=current_bazel_command, + build_flags=parsed_build_flags, + build_targets=parsed_build_targets, + persistent_compdb=persistent_compdb, + enterprise=enterprise, + atlas=atlas, + compiledb_targets=posthook_targets or None, + setup_clang_tidy=False, + ) + if skip_plus_interface and not autocomplete_query: return args[1:] diff --git a/buildscripts/setup_clang_tidy.py b/buildscripts/setup_clang_tidy.py index 9538ffd1e73..9670c0e83f6 100644 --- a/buildscripts/setup_clang_tidy.py +++ b/buildscripts/setup_clang_tidy.py @@ -15,31 +15,10 @@ PLUGIN_CANDIDATES = [ ] -def _linux_distribution_id_version() -> tuple[str | None, str | None]: - if platform.system() != "Linux": - return None, None - - os_release = pathlib.Path("/etc/os-release") - if not os_release.exists(): - return None, None - - metadata: dict[str, str] = {} - for line in os_release.read_text(encoding="utf-8").splitlines(): - if "=" not in line: - continue - key, value = line.split("=", 1) - metadata[key] = value.strip().strip('"').strip("'") - - return metadata.get("ID"), metadata.get("VERSION_ID") - - def mongo_tidy_checks_supported_platform() -> bool: if platform.system() != "Linux": return False - distro_id, version_id = _linux_distribution_id_version() - return not (distro_id == "ubuntu" and version_id == "18.04") - def clang_tidy_setup_recovery_message() -> str: if mongo_tidy_checks_supported_platform(): @@ -72,7 +51,9 @@ def materialize_clang_tidy_ide_files( plugin_src: pathlib.Path, ) -> tuple[bool, bool]: config_changed = _copy_if_changed(config_src, repo_root / ".clang-tidy") - marker_changed = _write_if_changed(repo_root / ".mongo_checks_module_path", str(plugin_src)) + marker_changed = _write_if_changed( + repo_root / ".mongo_checks_module_path", str(plugin_src.resolve()) + ) return config_changed, marker_changed diff --git a/buildscripts/tests/test_bazel_plus_test_interface.py b/buildscripts/tests/test_bazel_plus_test_interface.py index 48027d27318..dc123094388 100644 --- a/buildscripts/tests/test_bazel_plus_test_interface.py +++ b/buildscripts/tests/test_bazel_plus_test_interface.py @@ -414,26 +414,35 @@ class Tests(unittest.TestCase): args = ["wrapper_hook", "build", "--config=compiledb", "//src/mongo/base:error_codes"] generate_calls = [] + prepare_calls = [] def fake_generate_compiledb(*call_args, **call_kwargs): generate_calls.append((call_args, call_kwargs)) + def fake_prepare_posthook(*call_args, **call_kwargs): + prepare_calls.append((call_args, call_kwargs)) + return ["build", "--config=compiledb", "//src/mongo/base:error_codes"] + original_generate_compiledb = plus_interface.generate_compiledb + original_prepare_posthook = plus_interface.prepare_compiledb_posthook_args original_wrapper_config_mode_file = plus_interface.WRAPPER_CONFIG_MODE_FILE with tempfile.TemporaryDirectory() as tempdir: wrapper_config_mode_file = os.path.join(tempdir, "mongo_wrapper_config_mode") with open(wrapper_config_mode_file, "w", encoding="utf-8") as file_handle: file_handle.write("dbg") plus_interface.generate_compiledb = fake_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = fake_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = wrapper_config_mode_file try: result = test_runner_interface(args, False, buildozer_output) finally: plus_interface.generate_compiledb = original_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = original_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = original_wrapper_config_mode_file assert result == ["build", "--config=compiledb", "//src/mongo/base:error_codes"] assert len(generate_calls) == 0 + assert len(prepare_calls) == 1 def test_config_separate_compiledb_runs_normally(self): def buildozer_output(autocomplete_query): @@ -441,22 +450,35 @@ class Tests(unittest.TestCase): args = ["wrapper_hook", "build", "--config", "compiledb", "//src/mongo/base:error_codes"] generate_calls = [] + prepare_calls = [] def fake_generate_compiledb(*call_args, **call_kwargs): generate_calls.append((call_args, call_kwargs)) + def fake_prepare_posthook(*call_args, **call_kwargs): + prepare_calls.append((call_args, call_kwargs)) + return [ + "build", + "--config", + "compiledb", + "//src/mongo/base:error_codes", + ] + original_generate_compiledb = plus_interface.generate_compiledb + original_prepare_posthook = plus_interface.prepare_compiledb_posthook_args original_wrapper_config_mode_file = plus_interface.WRAPPER_CONFIG_MODE_FILE with tempfile.TemporaryDirectory() as tempdir: wrapper_config_mode_file = os.path.join(tempdir, "mongo_wrapper_config_mode") with open(wrapper_config_mode_file, "w", encoding="utf-8") as file_handle: file_handle.write("dbg") plus_interface.generate_compiledb = fake_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = fake_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = wrapper_config_mode_file try: result = test_runner_interface(args, False, buildozer_output) finally: plus_interface.generate_compiledb = original_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = original_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = original_wrapper_config_mode_file assert result == [ @@ -466,6 +488,7 @@ class Tests(unittest.TestCase): "//src/mongo/base:error_codes", ] assert len(generate_calls) == 0 + assert len(prepare_calls) == 1 def test_config_separate_compiledb_runs_normally_with_plain_target(self): def buildozer_output(autocomplete_query): @@ -473,22 +496,35 @@ class Tests(unittest.TestCase): args = ["wrapper_hook", "build", "--config", "compiledb", "install-dist-test"] generate_calls = [] + prepare_calls = [] def fake_generate_compiledb(*call_args, **call_kwargs): generate_calls.append((call_args, call_kwargs)) + def fake_prepare_posthook(*call_args, **call_kwargs): + prepare_calls.append((call_args, call_kwargs)) + return [ + "build", + "--config", + "compiledb", + "install-dist-test", + ] + original_generate_compiledb = plus_interface.generate_compiledb + original_prepare_posthook = plus_interface.prepare_compiledb_posthook_args original_wrapper_config_mode_file = plus_interface.WRAPPER_CONFIG_MODE_FILE with tempfile.TemporaryDirectory() as tempdir: wrapper_config_mode_file = os.path.join(tempdir, "mongo_wrapper_config_mode") with open(wrapper_config_mode_file, "w", encoding="utf-8") as file_handle: file_handle.write("dbg") plus_interface.generate_compiledb = fake_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = fake_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = wrapper_config_mode_file try: result = test_runner_interface(args, False, buildozer_output) finally: plus_interface.generate_compiledb = original_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = original_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = original_wrapper_config_mode_file assert result == [ @@ -498,6 +534,7 @@ class Tests(unittest.TestCase): "install-dist-test", ] assert len(generate_calls) == 0 + assert len(prepare_calls) == 1 def test_config_separate_compiledb_runs_normally_with_target_before_config(self): def buildozer_output(autocomplete_query): @@ -505,22 +542,35 @@ class Tests(unittest.TestCase): args = ["wrapper_hook", "build", "install-dist-test", "--config", "compiledb"] generate_calls = [] + prepare_calls = [] def fake_generate_compiledb(*call_args, **call_kwargs): generate_calls.append((call_args, call_kwargs)) + def fake_prepare_posthook(*call_args, **call_kwargs): + prepare_calls.append((call_args, call_kwargs)) + return [ + "build", + "install-dist-test", + "--config", + "compiledb", + ] + original_generate_compiledb = plus_interface.generate_compiledb + original_prepare_posthook = plus_interface.prepare_compiledb_posthook_args original_wrapper_config_mode_file = plus_interface.WRAPPER_CONFIG_MODE_FILE with tempfile.TemporaryDirectory() as tempdir: wrapper_config_mode_file = os.path.join(tempdir, "mongo_wrapper_config_mode") with open(wrapper_config_mode_file, "w", encoding="utf-8") as file_handle: file_handle.write("dbg") plus_interface.generate_compiledb = fake_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = fake_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = wrapper_config_mode_file try: result = test_runner_interface(args, False, buildozer_output) finally: plus_interface.generate_compiledb = original_generate_compiledb + plus_interface.prepare_compiledb_posthook_args = original_prepare_posthook plus_interface.WRAPPER_CONFIG_MODE_FILE = original_wrapper_config_mode_file assert result == [ @@ -530,6 +580,7 @@ class Tests(unittest.TestCase): "compiledb", ] assert len(generate_calls) == 0 + assert len(prepare_calls) == 1 if __name__ == "__main__": diff --git a/buildscripts/tests/test_compiledb_output_format.py b/buildscripts/tests/test_compiledb_output_format.py index d4d61fb2c82..ac15f3e4b9a 100644 --- a/buildscripts/tests/test_compiledb_output_format.py +++ b/buildscripts/tests/test_compiledb_output_format.py @@ -3,10 +3,40 @@ import unittest sys.path.append(".") -from bazel.wrapper_hook.compiledb import _build_final_compile_command_entry +from bazel.wrapper_hook.compiledb import ( + SETUP_CLANG_TIDY_BUILD_TARGETS, + _build_final_compile_command_entry, + _resolve_extra_build_targets, +) class CompiledbOutputFormatTest(unittest.TestCase): + def test_setup_clang_tidy_targets_are_appended_once(self): + extra_build_targets = [ + "//src/mongo/base:error_codes", + SETUP_CLANG_TIDY_BUILD_TARGETS[0], + ] + + resolved_targets = _resolve_extra_build_targets( + extra_build_targets=extra_build_targets, + setup_clang_tidy=True, + ) + + assert resolved_targets == [ + "//src/mongo/base:error_codes", + *SETUP_CLANG_TIDY_BUILD_TARGETS, + ] + + def test_setup_clang_tidy_targets_are_not_added_when_disabled(self): + extra_build_targets = ["//src/mongo/base:error_codes"] + + resolved_targets = _resolve_extra_build_targets( + extra_build_targets=extra_build_targets, + setup_clang_tidy=False, + ) + + assert resolved_targets == extra_build_targets + def test_final_entry_omits_non_standard_target_key(self): def rewrite_exec_path(path, out_root_str, external_root_str): if path.startswith("bazel-out/"): diff --git a/buildscripts/tests/test_compiledb_posthook.py b/buildscripts/tests/test_compiledb_posthook.py new file mode 100644 index 00000000000..06e6ecb7f73 --- /dev/null +++ b/buildscripts/tests/test_compiledb_posthook.py @@ -0,0 +1,162 @@ +import os +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.append(".") + +from bazel.wrapper_hook.plus_interface import test_runner_interface + + +def _noop_buildozer(*_args, **_kwargs): + return "" + + +class CompiledbPosthookTest(unittest.TestCase): + """Verify that the compiledb posthook is set up from both the 'compiledb' target + and the '--config=compiledb' config flag.""" + + PATCHES = { + "generate": "bazel.wrapper_hook.plus_interface.generate_compiledb", + "prepare": "bazel.wrapper_hook.plus_interface.prepare_compiledb_posthook_args", + "clear": "bazel.wrapper_hook.plus_interface.clear_compiledb_posthook_state", + "swap": "bazel.wrapper_hook.plus_interface.swap_default_config", + } + + def _run(self, args, **kwargs): + with ( + mock.patch(self.PATCHES["generate"]) as mock_gen, + mock.patch(self.PATCHES["prepare"], return_value=["build", "--done"]) as mock_prep, + mock.patch(self.PATCHES["clear"]), + mock.patch(self.PATCHES["swap"], return_value=None), + mock.patch.dict(os.environ, {"CI": "1"}, clear=False), + ): + result = test_runner_interface( + args, + autocomplete_query=False, + get_buildozer_output=_noop_buildozer, + enterprise=True, + atlas=True, + **kwargs, + ) + return result, mock_gen, mock_prep + + # ------------------------------------------------------------------ + # compiledb TARGET triggers generate_compiledb (direct path) + # ------------------------------------------------------------------ + + def test_compiledb_target_calls_generate_compiledb(self): + result, mock_gen, mock_prep = self._run(["bazel", "build", "compiledb"]) + mock_gen.assert_called_once() + mock_prep.assert_not_called() + self.assertEqual(result, []) + + def test_compiledb_target_with_colon_calls_generate_compiledb(self): + result, mock_gen, mock_prep = self._run(["bazel", "build", ":compiledb"]) + mock_gen.assert_called_once() + mock_prep.assert_not_called() + + def test_compiledb_target_full_label_calls_generate_compiledb(self): + result, mock_gen, mock_prep = self._run(["bazel", "build", "//:compiledb"]) + mock_gen.assert_called_once() + mock_prep.assert_not_called() + + # ------------------------------------------------------------------ + # --config=compiledb triggers prepare_compiledb_posthook_args + # ------------------------------------------------------------------ + + def test_config_compiledb_calls_prepare_posthook(self): + result, mock_gen, mock_prep = self._run( + ["bazel", "build", "--config=compiledb", "//src/mongo/..."] + ) + mock_gen.assert_not_called() + mock_prep.assert_called_once() + call_kwargs = mock_prep.call_args + self.assertEqual(call_kwargs.kwargs["command"], "build") + self.assertFalse(call_kwargs.kwargs["setup_clang_tidy"]) + + def test_config_compiledb_aspect_calls_prepare_posthook(self): + result, mock_gen, mock_prep = self._run( + ["bazel", "build", "--config=compiledb-aspect", "//src/mongo/..."] + ) + mock_gen.assert_not_called() + mock_prep.assert_called_once() + + def test_config_compiledb_with_startup_args_forwards_them(self): + result, mock_gen, mock_prep = self._run( + [ + "bazel", + "--output_user_root=/tmp/cache", + "build", + "--config=compiledb", + "//src/mongo/...", + ] + ) + mock_prep.assert_called_once() + call_kwargs = mock_prep.call_args + self.assertEqual(call_kwargs.kwargs["startup_args"], ["--output_user_root=/tmp/cache"]) + + def test_config_compiledb_with_target_pattern_file(self): + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: + f.write("//src/mongo/db:mongod\n//src/mongo/s:mongos\n") + pattern_file = f.name + try: + result, mock_gen, mock_prep = self._run( + [ + "bazel", + "build", + "--config=compiledb", + f"--target_pattern_file={pattern_file}", + ] + ) + mock_prep.assert_called_once() + call_kwargs = mock_prep.call_args + self.assertEqual(call_kwargs.kwargs["build_targets"], []) + posthook_targets = call_kwargs.kwargs["compiledb_targets"] + self.assertIn("//src/mongo/db:mongod", posthook_targets) + self.assertIn("//src/mongo/s:mongos", posthook_targets) + finally: + os.unlink(pattern_file) + + def test_config_compiledb_returns_prepare_result(self): + result, _, _ = self._run(["bazel", "build", "--config=compiledb", "//src/mongo/..."]) + self.assertEqual(result, ["build", "--done"]) + + # ------------------------------------------------------------------ + # Plain build (no compiledb) triggers neither + # ------------------------------------------------------------------ + + def test_plain_build_skips_compiledb(self): + result, mock_gen, mock_prep = self._run(["bazel", "build", "//src/mongo/..."]) + mock_gen.assert_not_called() + mock_prep.assert_not_called() + self.assertEqual(result, ["build", "//src/mongo/..."]) + + # ------------------------------------------------------------------ + # compiledb target takes precedence over --config=compiledb + # ------------------------------------------------------------------ + + def test_compiledb_target_takes_precedence_over_config(self): + """When both 'compiledb' target and --config=compiledb appear, + generate_compiledb is called (target path), not prepare_posthook.""" + result, mock_gen, mock_prep = self._run( + ["bazel", "build", "--config=compiledb", "compiledb"] + ) + mock_gen.assert_called_once() + mock_prep.assert_not_called() + + # ------------------------------------------------------------------ + # Non-build commands with --config=compiledb don't trigger posthook + # ------------------------------------------------------------------ + + def test_config_compiledb_on_test_command_does_not_trigger_posthook(self): + result, mock_gen, mock_prep = self._run( + ["bazel", "test", "--config=compiledb", "//src/mongo/..."] + ) + mock_gen.assert_not_called() + mock_prep.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/evergreen/coverity_build.sh b/evergreen/coverity_build.sh index 04315c46ef9..32aba7e9015 100644 --- a/evergreen/coverity_build.sh +++ b/evergreen/coverity_build.sh @@ -64,6 +64,9 @@ printf ' %q' "${build_compiledb_command[@]}" echo "${build_compiledb_command[@]}" +echo "Setting up clang-tidy IDE files" +bazel $bazel_cache run $build_config //:setup_clang_tidy + compiledb_output_base="$(bazel $bazel_cache info output_base)" repo_python="" python_candidates=( diff --git a/evergreen/validate_compile_commands.py b/evergreen/validate_compile_commands.py index 20c607804cd..96d2e7f5b47 100644 --- a/evergreen/validate_compile_commands.py +++ b/evergreen/validate_compile_commands.py @@ -15,6 +15,14 @@ from typing import Any, Iterator STANDARD_COMPILE_COMMAND_KEYS = frozenset({"arguments", "command", "directory", "file", "output"}) COMPILEDB_GENERATION_TARGETS = ["compiledb", "install-wiredtiger"] +MONGO_TIDY_PLUGIN_CANDIDATES = frozenset( + [ + "libmongo_tidy_checks.so", + "libmongo_tidy_checks.dylib", + "mongo_tidy_checks.dll", + "libmongo_tidy_checks.dll", + ] +) def _get_workspace_dir() -> str: @@ -38,6 +46,48 @@ def _ensure_compiledb_exists(compdb_path: str) -> None: subprocess.run(["bazel", "build", *COMPILEDB_GENERATION_TARGETS], check=True) +def _mongo_tidy_checks_supported_platform() -> bool: + if platform.system() != "Linux": + return False + + +def _validate_clang_tidy_setup(workspace_dir: str) -> None: + if not _mongo_tidy_checks_supported_platform(): + return + + config_path = os.path.join(workspace_dir, ".clang-tidy") + if not os.path.isfile(config_path): + raise ValueError( + "Expected '.clang-tidy' to exist in the workspace root after generating " + "compile_commands.json on this platform." + ) + + plugin_marker_path = os.path.join(workspace_dir, ".mongo_checks_module_path") + if not os.path.isfile(plugin_marker_path): + raise ValueError( + "Expected '.mongo_checks_module_path' to exist in the workspace root after " + "generating compile_commands.json on this platform." + ) + + with open(plugin_marker_path, "r", encoding="utf-8") as marker_file: + plugin_path = marker_file.read().strip() + + if not plugin_path: + raise ValueError("'.mongo_checks_module_path' must contain a plugin path.") + + plugin_name = os.path.basename(plugin_path) + if plugin_name not in MONGO_TIDY_PLUGIN_CANDIDATES: + raise ValueError( + f"'.mongo_checks_module_path' points to an unexpected plugin file: {plugin_name}" + ) + + if not os.path.isfile(plugin_path): + raise ValueError( + f"The mongo_tidy_checks plugin file recorded in '.mongo_checks_module_path' " + f"does not exist: {plugin_path}" + ) + + def _parse_repo_env_from_bazelrc(bazelrc_path: str, var_name: str) -> str | None: """Extract --repo_env=FOO=... from a .bazelrc file (best-effort).""" if not os.path.exists(bazelrc_path): @@ -718,6 +768,11 @@ def main() -> int: cli_args = _parse_args() compdb_path = "compile_commands.json" _ensure_compiledb_exists(compdb_path) + try: + _validate_clang_tidy_setup(workspace_dir) + except ValueError as e: + sys.stderr.write(f"ERROR: {e}\n") + return 1 try: selection_count = _determine_selection_count( default_count=10, diff --git a/evergreen/validate_compile_commands_test.py b/evergreen/validate_compile_commands_test.py index 357a5929740..cc59886b25b 100644 --- a/evergreen/validate_compile_commands_test.py +++ b/evergreen/validate_compile_commands_test.py @@ -11,6 +11,60 @@ import validate_compile_commands as validator class ValidateCompileCommandsTest(unittest.TestCase): + def test_validate_clang_tidy_setup_skips_unsupported_platforms(self): + with tempfile.TemporaryDirectory() as workspace_dir: + with mock.patch.object( + validator, "_mongo_tidy_checks_supported_platform", return_value=False + ): + validator._validate_clang_tidy_setup(workspace_dir) + + def test_validate_clang_tidy_setup_accepts_expected_files(self): + with tempfile.TemporaryDirectory() as workspace_dir: + plugin_dir = os.path.join(workspace_dir, "bazel-bin", "src", "mongo", "tools") + os.makedirs(plugin_dir, exist_ok=True) + plugin_path = os.path.join(plugin_dir, "libmongo_tidy_checks.so") + with open(os.path.join(workspace_dir, ".clang-tidy"), "w", encoding="utf-8") as f: + f.write("Checks: '*'\n") + with open(plugin_path, "w", encoding="utf-8") as f: + f.write("plugin") + with open( + os.path.join(workspace_dir, ".mongo_checks_module_path"), "w", encoding="utf-8" + ) as f: + f.write(plugin_path) + + with mock.patch.object( + validator, "_mongo_tidy_checks_supported_platform", return_value=True + ): + validator._validate_clang_tidy_setup(workspace_dir) + + def test_validate_clang_tidy_setup_rejects_missing_config(self): + with tempfile.TemporaryDirectory() as workspace_dir: + with mock.patch.object( + validator, "_mongo_tidy_checks_supported_platform", return_value=True + ): + with self.assertRaisesRegex(ValueError, r"Expected '\.clang-tidy' to exist"): + validator._validate_clang_tidy_setup(workspace_dir) + + def test_validate_clang_tidy_setup_rejects_missing_plugin(self): + with tempfile.TemporaryDirectory() as workspace_dir: + missing_plugin_path = os.path.join( + workspace_dir, "bazel-bin", "src", "mongo", "tools", "libmongo_tidy_checks.so" + ) + with open(os.path.join(workspace_dir, ".clang-tidy"), "w", encoding="utf-8") as f: + f.write("Checks: '*'\n") + with open( + os.path.join(workspace_dir, ".mongo_checks_module_path"), "w", encoding="utf-8" + ) as f: + f.write(missing_plugin_path) + + with mock.patch.object( + validator, "_mongo_tidy_checks_supported_platform", return_value=True + ): + with self.assertRaisesRegex( + ValueError, r"The mongo_tidy_checks plugin file recorded" + ): + validator._validate_clang_tidy_setup(workspace_dir) + def test_accepts_standard_arguments_entry(self): validator._validate_compiledb_entry( {