mongo/buildscripts/cltcache/cltcache.py.txt
Daniel Moody eaeaa0f8f3 SERVER-105866 add vscode clang tidy limiter and caching (#36873)
GitOrigin-RevId: 7e108ca0b458cbbe3fb1126f1da79a9d5984f68d
2025-06-10 14:40:46 +00:00

215 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""
A simple clang-tidy caching application. Prefix calls to clang-tidy to cache
their results for faster static code analysis.
"""
import gzip
import hashlib
import os
import pathlib
import re
import subprocess
import sys
import time
import configparser
def save_to_file_raw(data, filename):
with open(filename, "wb") as f:
f.write(data)
def save_to_file(string, filename):
save_to_file_raw(string.encode("utf-8"), filename)
def compress_to_file(data, filename):
save_to_file_raw(gzip.compress(data), filename)
def read_from_file_raw(filename):
with open(filename, "rb") as f:
return f.read()
def read_from_file(filename):
return read_from_file_raw(filename).decode("utf-8")
def decompress_from_file(filename):
return gzip.decompress(read_from_file_raw(filename))
def sha256(string):
m = hashlib.sha256()
m.update(string.encode('utf-8'))
return m.hexdigest()
def file_age(filepath):
return time.time() - os.path.getmtime(filepath)
def run_command(command):
return subprocess.run(command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, check=False)
def run_get_stdout(command, ignore_errors=False):
"""
Run a command and forward its stdout and stderr if exit code is nonzero,
otherwise return stdout.
"""
result = run_command(command)
if not ignore_errors and result.returncode != 0:
raise Exception(f"Bad exit code when running: {command}")
return result.stdout.decode("utf-8")
def remove_o_flag(compile_args):
if "-o" in compile_args:
oflag_index = compile_args.index('-o')
return compile_args[:oflag_index] + compile_args[oflag_index + 2:]
return compile_args
def postprocess_source(source, config):
def hash_replace(match):
return match.group(0).replace(match.group(1), len(match.group(1)) * "0")
replacements = []
if config.get("preprocessor", "strip_string_versions", fallback=True):
replacements.append(
(r'("[^"^\n]*?)([0-9]+(\.[0-9]+)+)', r'\1<version>'))
if config.get("preprocessor", "strip_string_hex_hashes", fallback=True):
replacements.append((r'"[^"^\n]*?([0-9a-fA-F]{5,128})', hash_replace))
for pattern, replacement in replacements:
changedSource = re.sub(pattern, replacement, source)
attempts = 0
while changedSource != source and attempts < 20:
source = changedSource
changedSource = re.sub(pattern, replacement, source)
attempts += 1
return source
def get_preproc_hash(compile_args, config):
compile_args = remove_o_flag(compile_args)
preproc_flag = "-E"
keep_comments_flag = config.get(
"preprocessor", "preserve_comments", fallback="-C")
preproc_command = config.get("preprocessor", "command", fallback="c++")
preproc_source = run_get_stdout(
[preproc_command] + compile_args + [preproc_flag, keep_comments_flag],
config.getboolean("preprocessor", "ignore_errors", fallback=False))
verbose = config.getboolean("behavior", "verbose", fallback=False)
if verbose:
print("cltcache length of preproccesed source:", len(preproc_source))
postproc_source = postprocess_source(preproc_source, config)
if verbose:
print("cltcache length of postproccesed source:", len(postproc_source))
preproc_hash = sha256(postproc_source)
return preproc_hash
def compute_cache_key(clang_tidy_call, config):
clang_tidy = clang_tidy_call[0]
if "--" not in clang_tidy_call:
raise Exception("Missing '--' flag in compiler options")
forwardflag_index = clang_tidy_call.index("--")
compile_args = clang_tidy_call[forwardflag_index + 1:]
clang_tidy_args = clang_tidy_call[1:forwardflag_index]
preproc_hash = get_preproc_hash(compile_args, config)
version_out = run_get_stdout([clang_tidy] + ["--version"])
version = ",".join(re.findall(r'[0-9]+\.[0-9]+\.?[0-9]*', version_out))
version_hash = sha256(version)
clang_tidy_config = run_get_stdout(
[clang_tidy] + clang_tidy_args + ["--dump-config"])
clang_tidy_config_hash = sha256(clang_tidy_config)
return sha256(preproc_hash + clang_tidy_config_hash + version_hash)[:-16]
def init_cltcache():
cltcache_path = os.environ.get(
"CLTCACHE_DIR", pathlib.Path().home() / ".cltcache")
cltcache_path.mkdir(parents=True, exist_ok=True)
config = configparser.ConfigParser()
config.read(cltcache_path / "cltcache.cfg")
return cltcache_path, config
def cache_clang_tidy(clang_tidy_call):
cltcache_path, config = init_cltcache()
cache_path, cat_path, out_path, err_path = (None, None, None, None)
verbose = config.getboolean("behavior", "verbose", fallback=False)
if verbose:
print("cltcache computing cache key")
try:
cache_key = compute_cache_key(clang_tidy_call, config)
if verbose:
print("cltcache key:", cache_key)
cat_path = cltcache_path / cache_key[0]
cache_path = cat_path / cache_key
out_path = cache_path.with_suffix(".out.gz")
err_path = cache_path.with_suffix(".err.gz")
if os.path.exists(cache_path):
if verbose:
print("cltcache hit!")
if os.path.exists(out_path):
clang_tidy_stdout = decompress_from_file(out_path)
if clang_tidy_stdout:
print(clang_tidy_stdout.decode("utf-8"),)
if os.path.exists(err_path):
clang_tidy_stderr = decompress_from_file(err_path)
if clang_tidy_stderr:
print(clang_tidy_stderr.decode("utf-8"), file=sys.stderr,)
sys.exit(int(read_from_file(cache_path)))
elif verbose:
print("cltcache miss...")
except Exception as e:
if verbose:
print("cltcache", e)
print(
"cltcache Preprocessing failed! Forwarding call without caching...",
file=sys.stderr)
result = run_command(clang_tidy_call)
clt_success = result.returncode == 0
preproc_success = cache_path is not None
cache_results = (clt_success or config.getboolean(
"behavior", "cache_failure", fallback=True)) and preproc_success
if cache_results:
cat_path.mkdir(parents=True, exist_ok=True)
if result.stdout:
print(result.stdout.decode("utf-8"),)
if cache_results:
compress_to_file(result.stdout, out_path)
if result.stderr:
print(result.stderr.decode("utf-8"), file=sys.stderr,)
if cache_results:
compress_to_file(result.stderr, err_path)
if cache_results:
if verbose:
print("cltcache caching results...")
save_to_file(str(result.returncode), cache_path)
sys.exit(result.returncode)
def main():
if len(sys.argv) <= 1:
command = sys.argv[0]
helptext = ("Usage:\n"
f" {command}\n"
f" {command} clang-tidy [clang-tidy options] -- "
"-o output [compiler options]")
print(helptext)
else:
cache_clang_tidy(sys.argv[1:])
if __name__ == "__main__":
main()