Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cpp_linter_hooks/clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions tests/test_clang_tidy.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
_looks_like_msvc_error,
_positive_int,
_repair_check_args,
_set_macos_sdkroot,
_split_source_files,
_strip_noise,
main,
Expand Down Expand Up @@ -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
Loading