|  | #!/usr/bin/env vpython3 | 
|  |  | 
|  | # Copyright (c) 2025 The WebRTC project authors. All Rights Reserved. | 
|  | # | 
|  | # Use of this source code is governed by a BSD-style license | 
|  | # that can be found in the LICENSE file in the root of the source | 
|  | # tree. An additional intellectual property rights grant can be found | 
|  | # in the file PATENTS.  All contributing project authors may | 
|  | # be found in the AUTHORS file in the root of the source tree. | 
|  | # | 
|  | # Run clang-tidy in the webrtc source directory. | 
|  | # The list of checks that is getting applied is in the toplevel | 
|  | # .clang-tidy file. To add a new check, add it to that file and run | 
|  | # the script. | 
|  | # | 
|  | # clang-tidy needs to be added to the .gclient file: | 
|  | # Example .gclient file: | 
|  | # solutions = [ | 
|  | #  { | 
|  | #    "name": "src", | 
|  | #    "url": "https://webrtc.googlesource.com/src.git", | 
|  | #    "deps_file": "DEPS", | 
|  | #    "managed": False, | 
|  | #    "custom_deps": {}, | 
|  | #    "custom_vars" : { | 
|  | #      "checkout_clangd": True, | 
|  | #      "checkout_clang_tidy": True, | 
|  | #    } | 
|  | #  }, | 
|  | # ] | 
|  | # | 
|  | # See also | 
|  | # https://chromium.googlesource.com/chromium/src/+/main/docs/clang_tidy.md | 
|  |  | 
|  | import argparse | 
|  | import time | 
|  | import pathlib | 
|  | import subprocess | 
|  |  | 
|  | _DEFAULT_WORKDIR = pathlib.Path("out/Default") | 
|  |  | 
|  | # This is relative to src dir. | 
|  | _TIDY_BUILD = "tools/clang/scripts/build_clang_tools_extra.py" | 
|  | # These are relative to the work dir so the path needs to be constructed later. | 
|  | _LLVM = "tools/clang/third_party/llvm/" | 
|  | _TIDY_RUNNER = _LLVM + "clang-tools-extra/clang-tidy/tool/run-clang-tidy.py" | 
|  | _TIDY_BINARY = _LLVM + "build/bin/clang-tidy" | 
|  | _REPLACEMENTS_BINARY = _LLVM + "build/bin/clang-apply-replacements" | 
|  |  | 
|  | def _valid_dir(path: str) -> pathlib.Path: | 
|  | """Checks if the given path is an existing dir | 
|  | relative to the current working directory. | 
|  |  | 
|  | Args: | 
|  | path: Relative dir path to the current working directory | 
|  |  | 
|  | Returns: | 
|  | pathlib.Path object wrapping the dir path | 
|  |  | 
|  | Raises: | 
|  | ValueError: If the dir doesn't exist | 
|  | """ | 
|  | pathlib_handle = pathlib.Path(path) | 
|  | if not pathlib_handle.is_dir(): | 
|  | raise ValueError(f"Dir path {pathlib_handle} does not exist!") | 
|  | return pathlib_handle | 
|  |  | 
|  |  | 
|  | def _build_clang_tools(work_dir: pathlib.Path) -> None: | 
|  | if pathlib.Path(work_dir, _TIDY_RUNNER).exists() and pathlib.Path( | 
|  | work_dir, _TIDY_BINARY).exists() and pathlib.Path( | 
|  | work_dir, _REPLACEMENTS_BINARY).exists(): | 
|  | # Assume that tidy updates at least once every 30 days, and | 
|  | # recompile it if it's more than 30 days old. | 
|  | tidy_binary_path = pathlib.Path(work_dir, _TIDY_BINARY) | 
|  | age_in_seconds = time.time() - tidy_binary_path.stat().st_mtime | 
|  | age_in_days = age_in_seconds / (24 * 60 * 60) | 
|  | if age_in_days < 30: | 
|  | return | 
|  | print("Binary is %d days old - recompiling" % age_in_days) | 
|  | print("Fetching and building clang-tidy") | 
|  | build_clang_tools_cmd = (_TIDY_BUILD, "--fetch", work_dir, "clang-tidy", | 
|  | "clang-apply-replacements") | 
|  | subprocess.run(build_clang_tools_cmd, | 
|  | capture_output=False, | 
|  | text=True, | 
|  | check=True) | 
|  |  | 
|  |  | 
|  | def _generate_compile_commands(work_dir: pathlib.Path) -> None: | 
|  | """Automatically generates the compile_commands.json file to be used | 
|  | by the include cleaner binary. | 
|  |  | 
|  | Args: | 
|  | work_dir: gn out dir where the compile_commands json file exists | 
|  | """ | 
|  | compile_commands_path = work_dir / "compile_commands.json" | 
|  | print("Generating compile commands file...") | 
|  | subprocess.run( | 
|  | ["tools/clang/scripts/generate_compdb.py", "-p", work_dir], | 
|  | stdout=compile_commands_path.open(mode="w+"), | 
|  | check=True, | 
|  | ) | 
|  |  | 
|  |  | 
|  | def _run_clang_tidy(work_dir: pathlib.Path) -> None: | 
|  | clang_tidy_cmd = (work_dir / _TIDY_RUNNER, "-p", work_dir, | 
|  | "-allow-no-checks", "-clang-tidy-binary", work_dir / | 
|  | _TIDY_BINARY, "-clang-apply-replacements-binary", | 
|  | work_dir / _REPLACEMENTS_BINARY, "-fix") | 
|  | subprocess.run(clang_tidy_cmd, | 
|  | capture_output=False, | 
|  | text=True, | 
|  | check=False) | 
|  |  | 
|  |  | 
|  | def _parse_args() -> argparse.Namespace: | 
|  | parser = argparse.ArgumentParser( | 
|  | description="Runs clang-tidy with a set of rules", | 
|  | formatter_class=argparse.ArgumentDefaultsHelpFormatter, | 
|  | ) | 
|  | parser.add_argument( | 
|  | "-w", | 
|  | "--work-dir", | 
|  | type=_valid_dir, | 
|  | default=str(_DEFAULT_WORKDIR), | 
|  | help="Specify the gn workdir", | 
|  | ) | 
|  |  | 
|  | return parser.parse_args() | 
|  |  | 
|  |  | 
|  | def main() -> None: | 
|  | args = _parse_args() | 
|  | _build_clang_tools(args.work_dir) | 
|  | _generate_compile_commands(args.work_dir) | 
|  | _run_clang_tidy(args.work_dir) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |