Skip to content

Commit f7bc9c3

Browse files
committed
[bazel] Add aspects to run clang-tidy
This commit adds "fix" and "check" aspects that run clang-tidy. Example usage: ./bazelisk.sh build --config=clang_tidy_fix \ --config=riscv32 //sw/device/lib/base:memory ./bazelisk.sh build --config=clang_tidy_check \ --config=riscv32 //sw/device/lib/base:memory This also adds a new target, //quality:clang_tidy_check, which runs clang-tidy against a very incomplete list of hand-picked targets. Signed-off-by: Dan McArdle <[email protected]>
1 parent 10774b1 commit f7bc9c3

File tree

4 files changed

+357
-1
lines changed

4 files changed

+357
-1
lines changed

.bazelrc

+14
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ test:local_test_jobs_per_cpus --local_test_jobs=HOST_CPUS*0.22
7272
# We have verilator tests that take more than an hour to complete
7373
test --test_timeout=60,300,1500,7200
7474

75+
# Configuration to run clang-tidy alongside build actions. This is a strict
76+
# analysis, which will fail the build when any of its checks fail. Enable with
77+
# `--config=clang_tidy_check`.
78+
build:clang_tidy_check --aspects rules/quality.bzl%clang_tidy_check_aspect
79+
build:clang_tidy_check --output_groups=clang_tidy
80+
81+
# Configuration to run clang-tidy alongside build actions. In this mode, fixes
82+
# will be applied automatically when possible. This is not a strict analysis;
83+
# clang-tidy errors will not fail the build. Enable with
84+
# `--config=clang_tidy_check`.
85+
build:clang_tidy_fix --aspects rules/quality.bzl%clang_tidy_fix_aspect
86+
build:clang_tidy_fix --output_groups=clang_tidy
87+
build:clang_tidy_fix --spawn_strategy=local
88+
7589
# AddressSanitizer (ASan) catches runtime out-of-bounds accesses to globals, the
7690
# stack, and (less importantly for OpenTitan) the heap. ASan instruments
7791
# programs at compile time and also requires a runtime library.

quality/BUILD.bazel

+40-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
load("@ot_hack_bazelbuild_buildtools//buildifier:def.bzl", "buildifier", "buildifier_test")
66
load("@lowrisc_lint//rules:rules.bzl", "licence_test")
7-
load("//rules:quality.bzl", "clang_format_check", "clang_format_test", "html_coverage_report")
7+
load("//rules:quality.bzl", "clang_format_check", "clang_format_test", "clang_tidy_check", "clang_tidy_check_rv", "html_coverage_report")
88
load("@rules_rust//rust:defs.bzl", "rustfmt_test")
99

1010
package(default_visibility = ["//visibility:public"])
@@ -103,6 +103,45 @@ clang_format_check(
103103
mode = "fix",
104104
)
105105

106+
# TODO(dmcardle) Add more targets that should be automatically checked with
107+
# clang-tidy. Find a way to automate this.
108+
clang_tidy_check_rv(
109+
name = "clang_tidy_check_rv",
110+
testonly = True,
111+
deps = [
112+
"//sw/device/lib/base:memory",
113+
"//sw/device/lib/base:memory_perftest",
114+
"//sw/device/silicon_creator/rom:boot_policy",
115+
"//sw/device/silicon_creator/rom:bootstrap",
116+
"//sw/device/silicon_creator/rom:rom_common",
117+
"//sw/device/silicon_creator/rom:rom_epmp",
118+
"//sw/device/silicon_creator/rom:rom_start",
119+
"//sw/device/silicon_creator/rom:sigverify_keys",
120+
"//sw/device/silicon_creator/rom:sigverify_keys_rsa",
121+
"//sw/device/silicon_creator/rom:sigverify_keys_spx",
122+
"//sw/device/silicon_creator/rom/keys/fake",
123+
"//sw/device/silicon_creator/rom/keys/fake/spx:fake",
124+
],
125+
)
126+
127+
clang_tidy_check(
128+
name = "clang_tidy_check_host",
129+
testonly = True,
130+
deps = [
131+
"//sw/device/silicon_creator/rom:boot_policy_unittest",
132+
"//sw/device/silicon_creator/rom:bootstrap_unittest",
133+
],
134+
)
135+
136+
filegroup(
137+
name = "clang_tidy_check",
138+
testonly = True,
139+
srcs = [
140+
":clang_tidy_check_host",
141+
":clang_tidy_check_rv",
142+
],
143+
)
144+
106145
buildifier_exclude = [
107146
"./WORKSPACE", # Prevent Buildifier from inserting unnecessary newlines.
108147
"./**/vendor/**",

rules/quality.bzl

+205
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
"""Quality check rules for OpenTitan.
66
"""
77

8+
load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES", "C_COMPILE_ACTION_NAME")
89
load("@bazel_skylib//lib:shell.bzl", "shell")
10+
load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain")
11+
load("//rules:rv.bzl", "rv_rule")
912

1013
def _ensure_tag(tags, *tag):
1114
for t in tag:
@@ -95,6 +98,208 @@ def clang_format_test(**kwargs):
9598
kwargs["tags"] = _ensure_tag(tags, "no-sandbox", "no-cache", "external")
9699
_clang_format_test(**kwargs)
97100

101+
# To see which checks clang-tidy knows about, run this command:
102+
#
103+
# ./bazelisk.sh run @lowrisc_rv32imcb_files//:bin/clang-tidy -- --checks='*' --list-checks
104+
_CLANG_TIDY_CHECKS = [
105+
"clang-analyzer-core.*",
106+
]
107+
108+
def _clang_tidy_aspect_impl(target, ctx):
109+
"""Aspect implementation that runs clang-tidy on C/C++."""
110+
111+
if ctx.rule.kind not in ["cc_library", "cc_binary", "cc_test"]:
112+
return [OutputGroupInfo(clang_tidy = [])]
113+
114+
if CcInfo in target:
115+
cc_info = target[CcInfo]
116+
elif hasattr(ctx.rule.attr, "deps"):
117+
# Some rules, like cc_binary, do NOT produce a CcInfo provider. Therefore,
118+
# we need to build one from its dependencies.
119+
cc_info = cc_common.merge_cc_infos(
120+
direct_cc_infos = [dep[CcInfo] for dep in ctx.rule.attr.deps if CcInfo in dep],
121+
)
122+
else:
123+
return [OutputGroupInfo(clang_tidy = [])]
124+
cc_compile_ctx = cc_info.compilation_context
125+
126+
cc_toolchain = find_cc_toolchain(ctx).cc
127+
feature_configuration = cc_common.configure_features(
128+
ctx = ctx,
129+
cc_toolchain = cc_toolchain,
130+
requested_features = ctx.features,
131+
unsupported_features = ctx.disabled_features,
132+
)
133+
134+
if not hasattr(ctx.rule.attr, "srcs"):
135+
return [OutputGroupInfo(clang_tidy = [])]
136+
all_srcs = []
137+
for src in ctx.rule.attr.srcs:
138+
all_srcs += [src for src in src.files.to_list() if src.is_source]
139+
140+
outputs = []
141+
for src in all_srcs:
142+
if src.path.startswith("external/"):
143+
continue
144+
if not src.extension in ["c", "cc", "h"]:
145+
continue
146+
147+
generated_file = ctx.actions.declare_file("{}.{}.clang-tidy.out".format(src.basename, target.label.name))
148+
outputs.append(generated_file)
149+
150+
opts = ctx.fragments.cpp.copts
151+
if hasattr(ctx.rule.attr, "copts"):
152+
opts += ctx.rule.attr.copts
153+
154+
# TODO(dmcardle) What if an .h file should be compiled for C++? Perhaps
155+
# this should match the behavior in rules/cc_side_outputs.bzl.
156+
if src.extension in ["c", "h"]:
157+
opts += ctx.fragments.cpp.conlyopts
158+
else:
159+
opts += ctx.fragments.cpp.cxxopts
160+
if hasattr(ctx.rule.attr, "cxxopts"):
161+
opts += ctx.rule.attr.cxxopts
162+
163+
c_compile_variables = cc_common.create_compile_variables(
164+
feature_configuration = feature_configuration,
165+
cc_toolchain = cc_toolchain,
166+
source_file = src.path,
167+
user_compile_flags = opts,
168+
include_directories = depset(
169+
direct = [src.dirname for src in cc_compile_ctx.headers.to_list()],
170+
transitive = [cc_compile_ctx.includes],
171+
),
172+
quote_include_directories = cc_compile_ctx.quote_includes,
173+
system_include_directories = cc_compile_ctx.system_includes,
174+
framework_include_directories = cc_compile_ctx.framework_includes,
175+
preprocessor_defines = depset(
176+
direct = ctx.rule.attr.local_defines,
177+
transitive = [cc_compile_ctx.defines],
178+
),
179+
)
180+
181+
command_line = cc_common.get_memory_inefficient_command_line(
182+
feature_configuration = feature_configuration,
183+
action_name = ACTION_NAMES.c_compile,
184+
variables = c_compile_variables,
185+
)
186+
187+
args = ctx.actions.args()
188+
189+
# Add args that are consumed by the wrapper script.
190+
if ctx.attr._enable_fix:
191+
args.add("--ignore-clang-tidy-error")
192+
args.add(".clang-tidy.lock")
193+
args.add(generated_file)
194+
args.add_all(ctx.attr._clang_tidy.files)
195+
196+
# Add args for clang-tidy.
197+
if len(_CLANG_TIDY_CHECKS) > 0:
198+
checks_pattern = ",".join(_CLANG_TIDY_CHECKS)
199+
args.add("--checks=" + checks_pattern)
200+
201+
# Treat warnings from every enabled check as errors.
202+
args.add("--warnings-as-errors=" + checks_pattern)
203+
if ctx.attr._enable_fix:
204+
args.add("--fix")
205+
args.add("--fix-errors")
206+
207+
# Use the nearest .clang_format file to format code adjacent to fixes.
208+
args.add("--format-style=file")
209+
210+
# Specify a regex header filter. Without this, clang-tidy will not
211+
# report or fix errors in header files.
212+
args.add("--header-filter=.*\\.h$")
213+
args.add("--use-color")
214+
215+
for arg in command_line:
216+
# Skip the src file argument. We give that to clang-tidy separately.
217+
if arg == src.path:
218+
continue
219+
elif arg == "-fno-canonical-system-headers":
220+
continue
221+
args.add("--extra-arg=" + arg)
222+
223+
# Tell clang-tidy which source file to analyze.
224+
args.add(src)
225+
226+
ctx.actions.run(
227+
executable = ctx.attr._clang_tidy_wrapper.files_to_run,
228+
arguments = [args],
229+
inputs = depset(
230+
direct = [src],
231+
transitive = [
232+
cc_toolchain.all_files,
233+
cc_compile_ctx.headers,
234+
],
235+
),
236+
tools = [ctx.attr._clang_tidy.files_to_run],
237+
outputs = [generated_file],
238+
progress_message = "Running clang tidy on {}".format(src.path),
239+
)
240+
241+
return [
242+
OutputGroupInfo(
243+
clang_tidy = depset(direct = outputs),
244+
),
245+
]
246+
247+
def _make_clang_tidy_aspect(enable_fix):
248+
return aspect(
249+
implementation = _clang_tidy_aspect_impl,
250+
attr_aspects = ["deps"],
251+
attrs = {
252+
"_clang_tidy_wrapper": attr.label(
253+
default = "//rules/scripts:clang_tidy.py",
254+
allow_single_file = True,
255+
cfg = "host",
256+
executable = True,
257+
),
258+
"_clang_tidy": attr.label(
259+
default = "@lowrisc_rv32imcb_files//:bin/clang-tidy",
260+
allow_single_file = True,
261+
cfg = "host",
262+
executable = True,
263+
doc = "The clang-tidy executable",
264+
),
265+
"_enable_fix": attr.bool(default = enable_fix),
266+
},
267+
incompatible_use_toolchain_transition = True,
268+
fragments = ["cpp"],
269+
host_fragments = ["cpp"],
270+
toolchains = ["@rules_cc//cc:toolchain_type"],
271+
)
272+
273+
clang_tidy_fix_aspect = _make_clang_tidy_aspect(True)
274+
clang_tidy_check_aspect = _make_clang_tidy_aspect(False)
275+
276+
def _clang_tidy_check_impl(ctx):
277+
return [
278+
DefaultInfo(
279+
files = depset(
280+
transitive = [dep[OutputGroupInfo].clang_tidy for dep in ctx.attr.deps],
281+
),
282+
),
283+
]
284+
285+
clang_tidy_check_rv = rv_rule(
286+
implementation = _clang_tidy_check_impl,
287+
attrs = {
288+
"deps": attr.label_list(
289+
aspects = [clang_tidy_check_aspect],
290+
),
291+
},
292+
)
293+
294+
clang_tidy_check = rule(
295+
implementation = _clang_tidy_check_impl,
296+
attrs = {
297+
"deps": attr.label_list(
298+
aspects = [clang_tidy_check_aspect],
299+
),
300+
},
301+
)
302+
98303
def _html_coverage_report_impl(ctx):
99304
out_file = ctx.actions.declare_file(ctx.label.name + ".bash")
100305
substitutions = {}

rules/scripts/clang_tidy.py

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright lowRISC contributors.
4+
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
5+
# SPDX-License-Identifier: Apache-2.0
6+
"""
7+
This wrapper script acts as a shim between Bazel and clang-tidy.
8+
"""
9+
10+
import argparse
11+
import fcntl
12+
from pathlib import Path
13+
import subprocess
14+
import sys
15+
import time
16+
17+
18+
def maybe_rename_path(orig: Path, new: Path):
19+
"""Tries to rename `orig` to `new`. Returns an "undo" lambda.
20+
"""
21+
try:
22+
orig.rename(new)
23+
except FileNotFoundError:
24+
pass
25+
return lambda: maybe_rename_path(new, orig)
26+
27+
28+
def acquire_lock(path: Path):
29+
"""Blocks until acquiring an exclusive lock on `path`.
30+
"""
31+
with open(path, "a+") as f:
32+
while True:
33+
try:
34+
fcntl.flock(f, fcntl.LOCK_EX)
35+
return
36+
except IOError:
37+
time.sleep(0.1)
38+
39+
40+
def main() -> int:
41+
parser = argparse.ArgumentParser()
42+
parser.add_argument('--ignore-clang-tidy-error', action='store_true')
43+
parser.add_argument('--print-args', action='store_true')
44+
parser.add_argument('lock_file', type=Path)
45+
parser.add_argument('out_file', type=Path)
46+
parser.add_argument('clang_tidy')
47+
parser.add_argument('clang_tidy_args', nargs=argparse.REMAINDER)
48+
49+
args = parser.parse_args()
50+
51+
if args.print_args:
52+
print("Args that will be passed to clang-tidy:")
53+
for arg in args.clang_tidy_args:
54+
print(" ", arg)
55+
56+
# Acquire an exclusive lock before proceeding. There are two reasons for locking.
57+
#
58+
# (1) We need to ensure that the compilation database is not present when
59+
# clang-tidy runs. If compile_commands.json exists, clang-tidy will read
60+
# it and ignore the compiler arguments we give it. There does not appear
61+
# to be a command-line flag that controls this behavior, so all we can
62+
# do is ensure the file isn't present when clang-tidy runs.
63+
#
64+
# (2) Without locking, headers may receive the same fix multiple times. For
65+
# instance, strict prototype fixes might pile up on `void Foo();` to
66+
# create something like `void Foo(voidvoidvoid);`. This happens because
67+
# Bazel runs many instances of clang-tidy in parallel, and clang-tidy
68+
# has no synchronization mechanism.
69+
#
70+
# To run clang-tidy in parallel without locking, we would need to export
71+
# fixes and deduplicate before applying them. Unfortunately, we cannot apply
72+
# the fixes that `--export-fixes` creates because our toolchain doesn't
73+
# include the `clang-apply-replacements` binary.
74+
acquire_lock(args.lock_file)
75+
76+
compile_commands = Path("compile_commands.json")
77+
cleanup_func = maybe_rename_path(
78+
compile_commands, compile_commands.with_suffix(".tmp-clang-tidy"))
79+
80+
assert not compile_commands.exists()
81+
82+
clang_tidy_command = [args.clang_tidy] + args.clang_tidy_args
83+
proc = subprocess.run(clang_tidy_command)
84+
if proc.returncode != 0:
85+
print(f"clang-tidy exited with {proc.returncode}")
86+
87+
if not args.ignore_clang_tidy_error:
88+
cleanup_func()
89+
return proc.returncode
90+
91+
args.out_file.touch()
92+
93+
cleanup_func()
94+
return 0
95+
96+
97+
if __name__ == '__main__':
98+
sys.exit(main())

0 commit comments

Comments
 (0)