mongo/buildscripts/clang_tidy_vscode.py

180 lines
6.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Wraps clang tidy to include our custom checks.
This script acts as a wrapper for the `clang-tidy` tool, allowing it to include custom checks
defined in a shared object file (`libmongo_tidy_checks.so`). Additionally, it filters the files
to be checked, ensuring that only files within the `src/mongo` directory are processed, excluding
those within `src/mongo/db/modules/enterprise/src/streams/third_party`.
Input:
- The script expects command-line arguments that are passed to `clang-tidy`.
- These arguments can include file paths, options, and other parameters supported by `clang-tidy`.
Output:
- The script runs `clang-tidy` on the specified files and outputs the results.
- If no valid `.cpp` files are found, or if all `.cpp` files are located in the excluded directories,
the script skips running `clang-tidy`.
- Standard output and error from the `clang-tidy` process are captured and printed.
Expected Format:
- command line example: buildscripts/clang_tidy_vscode.py /path/to/file/filename --export-fixes=-
- buildscripts/clang_tidy_vscode.py /path/to/file/filename --export-fixes=-
"""
# TODO: if https://github.com/notskm/vscode-clang-tidy/pull/77#issuecomment-1422910143 is resolved then this script can be removed
import json
import multiprocessing
import os
import pathlib
import subprocess
import sys
import time
from mongo_toolchain import get_mongo_toolchain
CLTCONFIG = """
# This file is intended to document the configuration options available
[preprocessor]
# Compiler command used when running preprocessor
command=%s
# Ignore errors from preprocessor and use whatever output it generated as cache key
# May cause weird issues when no output is generated
ignore_errors=false
# "" => NOLINT-comments don't work properly
# "-C" => NOLINT-comments work for regular code, but not in preprocessor macro expansion
# "-CC" => NOLINT-comments should work everywhere, but valid code may fail preprocessor stage. Combine with ignore_errors if you are paranoid about issues with NOLINT-comments
preserve_comments=-C
# Increase cache hit rate by ignoring some types of string contents
strip_string_versions=true
strip_string_hex_hashes=true
[behavior]
# Cache results even when clang-tidy fails
cache_failure=true
# Print cltcache errors and info in stderr and stdout
verbose=false
"""
def get_half_cpu_mask():
num_cores = multiprocessing.cpu_count()
return max(1, num_cores // 2)
def count_running_clang_tidy(cmd_path):
try:
output = subprocess.check_output(["ps", "-axww", "-o", "pid=,command="], text=True)
return sum(1 for line in output.splitlines() if cmd_path in line)
except Exception as e:
print(f"WARNING: failed to check running clang-tidy processes: {e}")
return 0
def wait_for_available_slot(cmd_path, max_jobs, check_interval):
while True:
if count_running_clang_tidy(cmd_path) < max_jobs:
break
time.sleep(check_interval)
def main():
cltcache_path = pathlib.Path(__file__).parent / "cltcache" / "cltcache.py.txt"
toolchain = get_mongo_toolchain(version="v5", from_bazel=True)
clang_tidy_path = toolchain.get_tool_path("clang-tidy")
max_jobs = get_half_cpu_mask()
wait_for_available_slot(clang_tidy_path, max_jobs, check_interval=0.2)
clang_tidy_cmd = [clang_tidy_path]
checks_so = None
if os.path.exists(".mongo_checks_module_path"):
with open(".mongo_checks_module_path") as f:
checks_so = f.read().strip()
if checks_so and os.path.isfile(checks_so):
clang_tidy_cmd += [f"-load={checks_so}"]
else:
print("ERROR: failed to find mongo tidy checks, run `bazel build compiledb'")
sys.exit(1)
files_to_check = []
other_args = []
for arg in sys.argv[1:]:
if os.path.isfile(arg):
rel = os.path.relpath(arg, os.path.dirname(os.path.dirname(__file__)))
if (
(arg.endswith(".cpp") or arg.endswith(".h"))
and rel.startswith("src/mongo")
and not rel.startswith("src/mongo/db/modules/enterprise/src/streams/third_party")
):
files_to_check.append(rel)
else:
other_args.append(arg)
if not files_to_check:
return 0
if len(files_to_check) > 1:
print(
f"ERROR: more than one file passed: {files_to_check}, only running {files_to_check[0]}"
)
if not os.path.exists("compile_commands.json"):
print("ERROR: failed to find compile_commands.json, run 'bazel build compiledb'")
sys.exit(1)
with open("compile_commands.json") as f:
compdb = json.load(f)
compile_args = []
executable = None
for entry in compdb:
if entry["file"] == files_to_check[0]:
compile_args = entry["arguments"][1:]
executable = entry["arguments"][0]
try:
index = compile_args.index("-MD")
compile_args = compile_args[:index] + compile_args[index + 3 :]
except ValueError:
pass
break
# found a cpp file entry with exact compile args, cache it
if compile_args:
cfg_dir = pathlib.Path().home() / ".cltcache"
cfg_dir.mkdir(parents=True, exist_ok=True)
conf_file = cfg_dir / "cltcache.cfg"
new_content = CLTCONFIG % executable
if not conf_file.exists() or conf_file.read_text() != new_content:
conf_file.write_text(new_content)
full_cmd = (
[sys.executable, cltcache_path]
+ clang_tidy_cmd
+ files_to_check
+ other_args
+ ["--"]
+ compile_args
)
proc = subprocess.run(full_cmd, capture_output=True)
# probably a header, skip caching and let clang-tidy do its thing:
else:
proc = subprocess.run(clang_tidy_cmd + files_to_check + other_args, capture_output=True)
sys.stdout.buffer.write(proc.stdout)
sys.stderr.buffer.write(proc.stderr)
return proc.returncode
if __name__ == "__main__":
sys.exit(main())