From fd44068015ce65994296f5de311e2ca47a2819a0 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Wed, 4 Nov 2026 08:42:55 +0000 Subject: [PATCH] fix: point clang-tidy at the macOS SDK when SDKROOT is not set On macOS the standard headers live in the Xcode or Command Line Tools SDK and /usr/include does not exist. Apple clang finds the SDK itself, but clang-tidy built from upstream LLVM stops at "'ctype.h' file not found" unless SDKROOT or -isysroot names it. When SDKROOT is not set, set it from `xcrun --show-sdk-path` before running clang-tidy. Refs cpp-linter/clang-tools-static-binaries#161 --- cpp_linter_hooks/clang_tidy.py | 27 +++++++++++++++++ tests/test_clang_tidy.py | 54 ++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/cpp_linter_hooks/clang_tidy.py b/cpp_linter_hooks/clang_tidy.py index b35ca92..f5fd4da 100644 --- a/cpp_linter_hooks/clang_tidy.py +++ b/cpp_linter_hooks/clang_tidy.py @@ -397,6 +397,32 @@ def _append_guidance(output: str) -> str: return output.rstrip("\n") + separator + "\n\n".join(hints) +def _set_macos_sdkroot(verbose: bool) -> None: + """Point clang-tidy at the macOS SDK when nothing else does. + + On macOS the C and C++ standard headers are in the Xcode or Command Line + Tools SDK, and /usr/include does not exist. Apple's clang finds the SDK by + itself, but clang-tidy is built from upstream LLVM and stops at + "'ctype.h' file not found" unless SDKROOT or -isysroot names the SDK. + An -isysroot in the compile command still wins over SDKROOT. + """ + if sys.platform != "darwin" or os.environ.get("SDKROOT"): + return + try: + sdk = subprocess.run( + ["xcrun", "--show-sdk-path"], + capture_output=True, + encoding="utf-8", + check=True, + ).stdout.strip() + except (OSError, subprocess.CalledProcessError): + return + if sdk: + os.environ["SDKROOT"] = sdk + if verbose: + print(f"Using the macOS SDK in {sdk} (SDKROOT)", file=sys.stderr) + + def _exec_clang_tidy(command) -> tuple[int, str]: """Run clang-tidy and return (retval, output).""" try: @@ -684,6 +710,7 @@ def run_clang_tidy(args=None) -> tuple[int, str]: return 1, version_error # Run the executable that was resolved, not whatever comes first on PATH. executable = str(tool_path) if tool_path else "clang-tidy" + _set_macos_sdkroot(hook_args.verbose) # -quiet hides clang-tidy's statistics about suppressed warnings; --verbose # keeps clang-tidy's full output. diff --git a/tests/test_clang_tidy.py b/tests/test_clang_tidy.py index dbc3c69..b25706e 100644 --- a/tests/test_clang_tidy.py +++ b/tests/test_clang_tidy.py @@ -25,6 +25,7 @@ _looks_like_msvc_error, _positive_int, _repair_check_args, + _set_macos_sdkroot, _split_source_files, _strip_noise, main, @@ -1537,3 +1538,56 @@ def test_split_source_files_knows_pre_commit_cpp_types(name): ["-quiet"], ["a.cpp", name], ) + + +def test_set_macos_sdkroot_uses_xcrun(monkeypatch, capsys): + monkeypatch.setattr(clang_tidy.sys, "platform", "darwin") + monkeypatch.delenv("SDKROOT", raising=False) + completed = MagicMock(stdout="/Applications/Xcode.app/MacOSX.sdk\n") + with patch( + "cpp_linter_hooks.clang_tidy.subprocess.run", return_value=completed + ) as run: + _set_macos_sdkroot(verbose=True) + + assert run.call_args.args[0] == ["xcrun", "--show-sdk-path"] + assert os.environ["SDKROOT"] == "/Applications/Xcode.app/MacOSX.sdk" + assert "/Applications/Xcode.app/MacOSX.sdk" in capsys.readouterr().err + + +@pytest.mark.parametrize( + ("platform", "sdkroot"), + (("linux", None), ("win32", None), ("darwin", "/my/sdk")), +) +def test_set_macos_sdkroot_keeps_environment(monkeypatch, platform, sdkroot): + monkeypatch.setattr(clang_tidy.sys, "platform", platform) + if sdkroot is None: + monkeypatch.delenv("SDKROOT", raising=False) + else: + monkeypatch.setenv("SDKROOT", sdkroot) + with patch("cpp_linter_hooks.clang_tidy.subprocess.run") as run: + _set_macos_sdkroot(verbose=False) + + run.assert_not_called() + assert os.environ.get("SDKROOT") == sdkroot + + +@pytest.mark.parametrize( + "outcome", + ( + OSError("xcrun not found"), + subprocess.CalledProcessError(1, ["xcrun"]), + MagicMock(stdout="\n"), + ), +) +def test_set_macos_sdkroot_without_sdk(monkeypatch, outcome): + monkeypatch.setattr(clang_tidy.sys, "platform", "darwin") + monkeypatch.delenv("SDKROOT", raising=False) + kwargs = ( + {"return_value": outcome} + if isinstance(outcome, MagicMock) + else {"side_effect": outcome} + ) + with patch("cpp_linter_hooks.clang_tidy.subprocess.run", **kwargs): + _set_macos_sdkroot(verbose=True) + + assert "SDKROOT" not in os.environ