Skip to content

Commit 411294a

Browse files
authored
Scope clang-tidy to C++ targets (#355)
## Summary - Scope `make clang-tidy` to exact `cc_library` / `cc_test` labels for touched `envpool` packages instead of building `//...`. - Skip clang-tidy entirely when a change has no C++-relevant files, while global Bazel / third-party edits still fall back to all C++ targets. ## Test Plan - `ruff check scripts/clang_tidy_targets.py` - `ruff format --check scripts/clang_tidy_targets.py` - `python3 -m compileall scripts/clang_tidy_targets.py` - `brix ssh dev-0 -C -- "bash -lc "cd /root/code/envpool && make lint""` - On `dev-0`, combined clang-tidy over all 38 exact C++ targets completed without hanging.
1 parent dad453d commit 411294a

2 files changed

Lines changed: 127 additions & 1 deletion

File tree

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ CLANG_TIDY_MAJOR = 18
1919
CLANG_TIDY_BIN = clang-tidy-$(CLANG_TIDY_MAJOR)
2020
CLANG_TIDY_WRAPPER_DIR = $(HOME)/.cache/$(PROJECT_NAME)/bin
2121
PATH := $(CLANG_TIDY_WRAPPER_DIR):$(HOME)/go/bin:$(PATH)
22+
CLANG_TIDY_TARGET_RESOLVER = python3 scripts/clang_tidy_targets.py
2223

2324
# installation
2425

@@ -124,7 +125,13 @@ bazel-pip-requirement-release:
124125
cd third_party/pip_requirements && (cmp requirements.txt requirements-release-lock.txt || ln -sf requirements-release-lock.txt requirements.txt)
125126

126127
clang-tidy: clang-tidy-install bazel-pip-requirement-dev
127-
$(BAZEL) build $(BAZELOPT) //... --config=clang-tidy --config=test
128+
targets="$${CLANG_TIDY_TARGETS:-$$($(CLANG_TIDY_TARGET_RESOLVER) | tr '\n' ' ')}"; \
129+
if [ -z "$$targets" ]; then \
130+
echo "No clang-tidy-relevant C++ changes detected; skipping."; \
131+
exit 0; \
132+
fi; \
133+
echo "Running clang-tidy on: $$targets"; \
134+
$(BAZEL) build $(BAZELOPT) $$targets --config=clang-tidy --config=test
128135

129136
bazel-debug: bazel-install bazel-pip-requirement-dev
130137
$(BAZEL) run $(BAZELOPT) //:setup --config=debug -- bdist_wheel

scripts/clang_tidy_targets.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
"""Resolve the smallest safe Bazel target set for clang-tidy."""
3+
4+
from __future__ import annotations
5+
6+
import os
7+
import pathlib
8+
import subprocess
9+
import sys
10+
11+
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
12+
FULL_RUN_FILES = {
13+
".bazelrc",
14+
".clang-tidy",
15+
"BUILD",
16+
"WORKSPACE",
17+
"envpool/BUILD",
18+
"envpool/pip.bzl",
19+
"envpool/requirements.bzl",
20+
"envpool/workspace0.bzl",
21+
"envpool/workspace1.bzl",
22+
}
23+
FULL_RUN_PREFIXES = ("third_party/",)
24+
CPP_SUFFIXES = (".cc", ".h")
25+
CC_RULE_KIND = "cc_(library|test)"
26+
27+
28+
def _git(*args: str) -> str:
29+
return subprocess.check_output(
30+
["git", *args],
31+
cwd=REPO_ROOT,
32+
text=True,
33+
).strip()
34+
35+
36+
def _bazel(*args: str) -> str:
37+
env = os.environ.copy()
38+
env.setdefault("USE_BAZEL_VERSION", "8.6.0")
39+
return subprocess.check_output(
40+
["bazelisk", *args],
41+
cwd=REPO_ROOT,
42+
env=env,
43+
text=True,
44+
).strip()
45+
46+
47+
def _is_valid_commit(rev: str) -> bool:
48+
if not rev or set(rev) == {"0"}:
49+
return False
50+
return (
51+
subprocess.run(
52+
["git", "rev-parse", "--verify", f"{rev}^{{commit}}"],
53+
cwd=REPO_ROOT,
54+
stdout=subprocess.DEVNULL,
55+
stderr=subprocess.DEVNULL,
56+
text=True,
57+
check=False,
58+
).returncode
59+
== 0
60+
)
61+
62+
63+
def _infer_changed_files() -> list[str]:
64+
base_sha = os.environ.get("CLANG_TIDY_BASE_SHA", "").strip()
65+
head_sha = os.environ.get("CLANG_TIDY_HEAD_SHA", "").strip() or "HEAD"
66+
if not _is_valid_commit(base_sha):
67+
try:
68+
base_sha = _git("merge-base", "HEAD", "origin/main")
69+
except subprocess.CalledProcessError:
70+
base_sha = _git("rev-parse", "HEAD^")
71+
committed = _git("diff", "--name-only", base_sha, head_sha)
72+
working_tree = _git("diff", "--name-only", "HEAD")
73+
changed_files = {
74+
line
75+
for diff in (committed, working_tree)
76+
for line in diff.splitlines()
77+
if line
78+
}
79+
return sorted(changed_files)
80+
81+
82+
def _resolve_targets(changed_files: list[str]) -> list[str]:
83+
if not changed_files:
84+
return []
85+
86+
for path in changed_files:
87+
if path in FULL_RUN_FILES or path.startswith(FULL_RUN_PREFIXES):
88+
query = _bazel("query", f'kind("{CC_RULE_KIND}", //...)')
89+
return [line for line in query.splitlines() if line]
90+
91+
package_patterns: set[str] = set()
92+
for path in changed_files:
93+
parts = pathlib.PurePosixPath(path).parts
94+
if len(parts) < 2 or parts[0] != "envpool":
95+
continue
96+
if parts[1] == "python":
97+
continue
98+
if (
99+
path.endswith(CPP_SUFFIXES)
100+
or pathlib.PurePosixPath(path).name == "BUILD"
101+
):
102+
package_patterns.add(f"//envpool/{parts[1]}:*")
103+
104+
targets: set[str] = set()
105+
for pattern in sorted(package_patterns):
106+
query = _bazel("query", f'kind("{CC_RULE_KIND}", {pattern})')
107+
targets.update(line for line in query.splitlines() if line)
108+
return sorted(targets)
109+
110+
111+
def _main() -> int:
112+
changed_files = sys.argv[1:] or _infer_changed_files()
113+
for target in _resolve_targets(changed_files):
114+
print(target)
115+
return 0
116+
117+
118+
if __name__ == "__main__":
119+
raise SystemExit(_main())

0 commit comments

Comments
 (0)