Skip to content

Commit 5c79f5d

Browse files
committed
[ot] script/opentitan: ot_tidy: create new ot_tidy.py script
This script is intended to replace the `ot-tidy.sh` script for running clang-tidy on our part of the codebase. A `-j` flag is added to run clang-tidy jobs in parallel, using the run-clang-tidy script that comes with it. On my machine this speeds up checking files for CI from >3 minutes to ~30 seconds. The script also checks for some configuration issues, such as the C compiler not being set to clang when configuring QEMU - this causes some warnings to be enabled that are not recognised by clang and will show up as errors. Signed-off-by: Alice Ziuziakowska <[email protected]>
1 parent 90f2da7 commit 5c79f5d

File tree

1 file changed

+206
-0
lines changed

1 file changed

+206
-0
lines changed

scripts/opentitan/ot_tidy.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright (c) 2025 lowRISC contributors.
4+
# SPDX-License-Identifier: Apache2
5+
6+
"""
7+
'clang-tidy' wrapper script
8+
"""
9+
10+
import argparse
11+
import re
12+
import subprocess
13+
import sys
14+
from glob import glob
15+
from multiprocessing import cpu_count
16+
from os import chdir, execvp, scandir
17+
from os.path import abspath, dirname, isfile, join, relpath, splitext
18+
from shutil import which
19+
20+
# clang major version to check for
21+
CLANG_MAJOR = 20
22+
23+
# Directory we are in
24+
ot_scripts = dirname(abspath(__file__))
25+
26+
# QEMU root directory
27+
qemu_root = dirname(dirname(ot_scripts))
28+
29+
# Default build directory is 'build' in QEMU root
30+
default_build_dir = join(qemu_root, 'build')
31+
32+
# clang-tidy config file path
33+
clang_tidy_yml = join(ot_scripts, "clang-tidy.yml")
34+
35+
def ci_files():
36+
"""Get list of files that are checked in CI"""
37+
lst_files_list = []
38+
ci_files_list = []
39+
directory = join(ot_scripts, 'clang-tidy.d')
40+
for entry in scandir(directory):
41+
if entry.is_file() and splitext(entry)[1] == '.lst':
42+
lst_files_list.append(entry)
43+
for entry in lst_files_list:
44+
with open(entry, "r") as f:
45+
patterns = f.readlines()
46+
for pattern in patterns:
47+
ci_files_list += glob(pattern.strip(), root_dir=qemu_root)
48+
return ci_files_list
49+
50+
def uint(value):
51+
"""Argparse function to parse integer greater than 0"""
52+
value = int(value)
53+
if value <= 0:
54+
raise argparse.ArgumentTypeError(f"{value} is not an integer greater than 0.")
55+
return value
56+
57+
def eprint(*args, **kwargs):
58+
"""Wrapper for print to stderr with colour"""
59+
col = "\x1b[33m"
60+
reset = "\x1b[0m"
61+
print(col, end='', file=sys.stderr)
62+
print(*args, *kwargs, end='', file=sys.stderr)
63+
print(reset, file=sys.stderr)
64+
65+
66+
def check_compile_commands(build_dir):
67+
"""
68+
Check for 'compile_commands.json' in given build directory
69+
"""
70+
if not isfile(join(build_dir, "compile_commands.json")):
71+
eprint(f"compile_commands.json not found in directory '{build_dir}',")
72+
eprint("specify your build directory with '--build-dir', or make sure to run configure.")
73+
sys.exit(1)
74+
75+
def check_cc(build_dir):
76+
""" Check that the configured C compiler for the build is clang"""
77+
cc_found = False
78+
# look in <build_dir>/config.status and try to find CC variable
79+
with open(join(build_dir, "config.status"), "r") as f:
80+
lines = f.readlines()
81+
for line in lines:
82+
m = re.match("^CC='(.*)'$", line)
83+
if m is not None:
84+
ccs = [f"clang-{CLANG_MAJOR}", "clang"]
85+
if m.group(1) not in ccs:
86+
eprint(f"QEMU build was configured with CC '{m.group(1)}', not 'clang',")
87+
eprint("Unknown warning and argument errors may appear.")
88+
cc_found = True
89+
break
90+
if not cc_found:
91+
# no CC option in config.status
92+
eprint("No CC argument given to configure, re-configure with '--cc=...' and rebuild.")
93+
eprint("If host toolchain is not clang, unknown warning and argument errors may appear.")
94+
95+
def check_and_get_clang_tidy() -> str:
96+
"""Check for and return an appropriate 'clang-tidy'"""
97+
# Check for 'clang-tidy-{VERSION}' first and return if found
98+
if which(f"clang-tidy-{CLANG_MAJOR}"):
99+
return f"clang-tidy-{CLANG_MAJOR}"
100+
# Otherwise, look for 'clang-tidy' and check its version
101+
elif which("clang-tidy"):
102+
p = subprocess.run(["clang-tidy", "--version"], capture_output=True)
103+
if p.returncode != 0:
104+
eprint("'clang-tidy --version' failed.")
105+
sys.exit(1)
106+
version_line = p.stdout.decode().split("\n")[0]
107+
m = re.match("^.*LLVM version ([0-9]*)\\..*$", version_line)
108+
if m is None:
109+
eprint("Could not parse 'clang-tidy' version string.")
110+
sys.exit(1)
111+
if m.group(1) != str(CLANG_MAJOR):
112+
eprint(f"'clang-tidy' is major version {m.group(1)}, expected {CLANG_MAJOR}.")
113+
sys.exit(1)
114+
return "clang-tidy"
115+
else:
116+
eprint(f"No 'clang-tidy-{CLANG_MAJOR}' or suitable 'clang-tidy' in PATH.")
117+
sys.exit(1)
118+
119+
def check_and_get_run_clang_tidy() -> str:
120+
"""Check for and return an appropriate 'run-clang-tidy'"""
121+
# Check for 'run clang-tidy-{VERSION}' first and return if found
122+
if which(f"run-clang-tidy-{CLANG_MAJOR}"):
123+
return f"run-clang-tidy-{CLANG_MAJOR}"
124+
# 'run-clang-tidy' does not have a version flag, so return it if it exists
125+
elif which("run-clang-tidy"):
126+
return "run-clang-tidy"
127+
else:
128+
eprint(f"No 'run-clang-tidy-{CLANG_MAJOR}' or suitable 'run-clang-tidy' in PATH.")
129+
sys.exit(1)
130+
131+
def main():
132+
"""Parser and wrapper main"""
133+
134+
parser = argparse.ArgumentParser()
135+
136+
parser.add_argument('--build-dir', required=False, default=default_build_dir,
137+
help="""
138+
QEMU build directory.
139+
Defaults to 'build' in QEMU root
140+
"""
141+
)
142+
143+
parser.add_argument('-j', '--jobs', required=False, type=uint, nargs='?',
144+
default=1, const=cpu_count(),
145+
help="""
146+
Number of tidy jobs to run in parallel.
147+
Defaults to 1 if not specified, and all cores if -j is given with no number.
148+
"""
149+
)
150+
151+
parser.add_argument('--ci-files', required=False, action='store_true',
152+
help="Run on all files checked by CI")
153+
154+
parser.add_argument('-f', '--files', required=False, nargs="+", action='extend', default=[],
155+
help="Files to run tidy on")
156+
157+
args = parser.parse_args()
158+
159+
# resolve build dir to absolute path
160+
args.build_dir = abspath(args.build_dir)
161+
162+
# filter anything that could be interpreted as a flag by clang-tidy
163+
args.files = [f for f in args.files if not f.startswith('-')]
164+
165+
# change provided relative paths to be relative to QEMU root
166+
# (CI file paths are already relative to root)
167+
args.files = [relpath(abspath(f), start=qemu_root) for f in args.files]
168+
169+
# append ci files
170+
if args.ci_files:
171+
args.files += ci_files()
172+
173+
if len(args.files) == 0:
174+
eprint("No input files provided")
175+
sys.exit(1)
176+
177+
check_compile_commands(args.build_dir)
178+
check_cc(args.build_dir)
179+
180+
cmd = ""
181+
cmd_args = []
182+
# use a 'run-clang-tidy' if running in parallel.
183+
# the wrapper has slightly different commandline arguments
184+
if args.jobs == 1:
185+
cmd = check_and_get_clang_tidy()
186+
cmd_args = [
187+
cmd,
188+
"-p", args.build_dir,
189+
"--config-file", clang_tidy_yml
190+
]
191+
else:
192+
cmd = check_and_get_run_clang_tidy()
193+
cmd_args = [
194+
cmd,
195+
"-p", args.build_dir,
196+
"-config-file", clang_tidy_yml,
197+
"-j", str(args.jobs),
198+
"-quiet"
199+
]
200+
201+
cmd_args += args.files
202+
chdir(qemu_root)
203+
execvp(cmd, cmd_args)
204+
205+
if __name__ == '__main__':
206+
main()

0 commit comments

Comments
 (0)