Skip to content

Commit 9d32848

Browse files
committed
build: add clang-tidy wrapper script
1 parent e6c28a4 commit 9d32848

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed

.clang-tidy

11 KB
Binary file not shown.

scripts/run-clang-tidy.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/usr/bin/env python3
2+
# TheSuperHackers @build JohnsterID 15/09/2025 Add clang-tidy runner script for code quality analysis
3+
# TheSuperHackers @build bobtista 04/12/2025 Simplify script for PCH-free analysis builds
4+
5+
"""
6+
Clang-tidy runner script for GeneralsGameCode project.
7+
8+
This is a convenience wrapper that:
9+
- Auto-detects the clang-tidy analysis build (build/clang-tidy)
10+
- Filters source files by include/exclude patterns
11+
- Processes files in batches to handle Windows command-line limits
12+
- Provides progress reporting
13+
14+
For the analysis build to work correctly, it must be built WITHOUT precompiled headers.
15+
Run this first:
16+
cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja
17+
"""
18+
19+
import argparse
20+
import json
21+
import subprocess
22+
import sys
23+
from pathlib import Path
24+
from typing import List, Optional
25+
26+
27+
def find_project_root() -> Path:
28+
"""Find the project root directory."""
29+
current = Path(__file__).resolve().parent
30+
while current != current.parent:
31+
if (current / 'CMakeLists.txt').exists():
32+
return current
33+
current = current.parent
34+
raise RuntimeError("Could not find project root (no CMakeLists.txt found)")
35+
36+
37+
def find_compile_commands(build_dir: Optional[Path] = None) -> Path:
38+
"""Find compile_commands.json from the clang-tidy analysis build."""
39+
project_root = find_project_root()
40+
41+
if build_dir:
42+
if not build_dir.is_absolute():
43+
build_dir = project_root / build_dir
44+
compile_commands = build_dir / "compile_commands.json"
45+
if compile_commands.exists():
46+
return compile_commands
47+
raise FileNotFoundError(
48+
f"compile_commands.json not found in {build_dir}"
49+
)
50+
51+
# Use the dedicated clang-tidy build (PCH-free, required for correct analysis)
52+
clang_tidy_build = project_root / "build" / "clang-tidy"
53+
compile_commands = clang_tidy_build / "compile_commands.json"
54+
55+
if not compile_commands.exists():
56+
raise RuntimeError(
57+
"Clang-tidy build not found!\n\n"
58+
"Create the analysis build first:\n"
59+
" cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja\n\n"
60+
"Or specify a different build with --build-dir"
61+
)
62+
63+
return compile_commands
64+
65+
66+
def load_compile_commands(compile_commands_path: Path) -> List[dict]:
67+
"""Load and parse compile_commands.json."""
68+
try:
69+
with open(compile_commands_path, 'r') as f:
70+
return json.load(f)
71+
except (json.JSONDecodeError, IOError) as e:
72+
raise RuntimeError(f"Failed to load compile_commands.json: {e}")
73+
74+
75+
def filter_source_files(compile_commands: List[dict],
76+
include_patterns: List[str],
77+
exclude_patterns: List[str]) -> List[str]:
78+
"""Filter source files based on include/exclude patterns."""
79+
project_root = find_project_root()
80+
source_files = set()
81+
82+
for entry in compile_commands:
83+
file_path = Path(entry['file'])
84+
85+
# Convert to relative path for pattern matching
86+
try:
87+
rel_path = file_path.relative_to(project_root)
88+
except ValueError:
89+
continue # File outside project root
90+
91+
rel_path_str = str(rel_path)
92+
93+
if include_patterns:
94+
if not any(pattern in rel_path_str for pattern in include_patterns):
95+
continue
96+
97+
if any(pattern in rel_path_str for pattern in exclude_patterns):
98+
continue
99+
100+
if file_path.suffix in {'.cpp', '.cxx', '.cc', '.c'}:
101+
source_files.add(str(file_path))
102+
103+
return sorted(source_files)
104+
105+
106+
def run_clang_tidy(source_files: List[str],
107+
compile_commands_path: Path,
108+
extra_args: List[str],
109+
fix: bool = False) -> int:
110+
"""Run clang-tidy on source files in batches."""
111+
if not source_files:
112+
print("No source files to analyze.")
113+
return 0
114+
115+
# Process files in batches (Windows has ~8191 char command-line limit)
116+
BATCH_SIZE = 50
117+
total_files = len(source_files)
118+
batches = [source_files[i:i + BATCH_SIZE] for i in range(0, total_files, BATCH_SIZE)]
119+
120+
print(f"Running clang-tidy on {total_files} file(s) in {len(batches)} batch(es)...\n")
121+
122+
overall_returncode = 0
123+
for batch_num, batch in enumerate(batches, 1):
124+
cmd = [
125+
'clang-tidy',
126+
f'-p={compile_commands_path.parent}',
127+
]
128+
129+
if fix:
130+
cmd.append('--fix')
131+
132+
if extra_args:
133+
cmd.extend(extra_args)
134+
135+
cmd.extend(batch)
136+
137+
print(f"Batch {batch_num}/{len(batches)}: Analyzing {len(batch)} file(s)...")
138+
139+
try:
140+
result = subprocess.run(cmd, cwd=find_project_root())
141+
if result.returncode != 0:
142+
overall_returncode = result.returncode
143+
except FileNotFoundError:
144+
print("Error: clang-tidy not found. Please install LLVM/Clang.", file=sys.stderr)
145+
return 1
146+
except KeyboardInterrupt:
147+
print("\nInterrupted by user.")
148+
return 130
149+
150+
return overall_returncode
151+
152+
153+
def main():
154+
parser = argparse.ArgumentParser(
155+
description="Run clang-tidy on GeneralsGameCode project",
156+
formatter_class=argparse.RawDescriptionHelpFormatter,
157+
epilog="""
158+
Examples:
159+
# First-time setup: Create PCH-free analysis build
160+
cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja
161+
162+
# Analyze all source files
163+
python scripts/run-clang-tidy.py
164+
165+
# Analyze specific directory
166+
python scripts/run-clang-tidy.py --include Core/Libraries/
167+
168+
# Analyze with specific checks
169+
python scripts/run-clang-tidy.py --include GameClient/ -- -checks="-*,modernize-use-nullptr"
170+
171+
# Apply fixes (use with caution!)
172+
python scripts/run-clang-tidy.py --fix --include Keyboard.cpp -- -checks="-*,modernize-use-nullptr"
173+
174+
# Use different build directory
175+
python scripts/run-clang-tidy.py --build-dir build/win32-debug
176+
177+
Note: Requires a PCH-free build. Create with:
178+
cmake -B build/clang-tidy -DCMAKE_DISABLE_PRECOMPILE_HEADERS=ON -G Ninja
179+
"""
180+
)
181+
182+
parser.add_argument(
183+
'--build-dir', '-b',
184+
type=Path,
185+
help='Build directory with compile_commands.json (auto-detected if omitted)'
186+
)
187+
188+
parser.add_argument(
189+
'--include', '-i',
190+
action='append',
191+
default=[],
192+
help='Include files matching this pattern (can be used multiple times)'
193+
)
194+
195+
parser.add_argument(
196+
'--exclude', '-e',
197+
action='append',
198+
default=[],
199+
help='Exclude files matching this pattern (can be used multiple times)'
200+
)
201+
202+
parser.add_argument(
203+
'--fix',
204+
action='store_true',
205+
help='Apply suggested fixes automatically (use with caution!)'
206+
)
207+
208+
parser.add_argument(
209+
'clang_tidy_args',
210+
nargs='*',
211+
help='Additional arguments to pass to clang-tidy (use after --)'
212+
)
213+
214+
args = parser.parse_args()
215+
216+
try:
217+
compile_commands_path = find_compile_commands(args.build_dir)
218+
print(f"Using compile commands: {compile_commands_path}\n")
219+
220+
compile_commands = load_compile_commands(compile_commands_path)
221+
222+
default_excludes = [
223+
'Dependencies/MaxSDK', # External SDK
224+
'_deps/', # CMake dependencies
225+
'build/', # Build artifacts
226+
'.git/', # Git directory
227+
]
228+
229+
exclude_patterns = default_excludes + args.exclude
230+
231+
source_files = filter_source_files(
232+
compile_commands,
233+
args.include,
234+
exclude_patterns
235+
)
236+
237+
if not source_files:
238+
print("No source files found matching the criteria.")
239+
return 1
240+
241+
print(f"Found {len(source_files)} source file(s) to analyze\n")
242+
243+
return run_clang_tidy(
244+
source_files,
245+
compile_commands_path,
246+
args.clang_tidy_args,
247+
args.fix
248+
)
249+
250+
except Exception as e:
251+
print(f"Error: {e}", file=sys.stderr)
252+
return 1
253+
254+
255+
if __name__ == '__main__':
256+
sys.exit(main())
257+

0 commit comments

Comments
 (0)