mirror of
				https://github.com/PaddlePaddle/FastDeploy.git
				synced 2025-10-31 11:56:44 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			498 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			498 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | |
| 
 | |
| # Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| #
 | |
| # ===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===#
 | |
| #
 | |
| # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 | |
| # See https://llvm.org/LICENSE.txt for license information.
 | |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 | |
| #
 | |
| # ===------------------------------------------------------------------------===#
 | |
| # FIXME: Integrate with clang-tidy-diff.py
 | |
| 
 | |
| """
 | |
| Parallel clang-tidy runner
 | |
| ==========================
 | |
| 
 | |
| Runs clang-tidy over all files in a compilation database. Requires clang-tidy
 | |
| and clang-apply-replacements in $PATH.
 | |
| 
 | |
| Example invocations.
 | |
| - Run clang-tidy on all files in the current working directory with a default
 | |
|   set of checks and show warnings in the cpp files and all project headers.
 | |
|     run-clang-tidy.py $PWD
 | |
| 
 | |
| - Fix all header guards.
 | |
|     run-clang-tidy.py -fix -checks=-*,llvm-header-guard
 | |
| 
 | |
| - Fix all header guards included from clang-tidy and header guards
 | |
|   for clang-tidy headers.
 | |
|     run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \
 | |
|                       -header-filter=extra/clang-tidy
 | |
| 
 | |
| Compilation database setup:
 | |
| http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
 | |
| """
 | |
| 
 | |
| 
 | |
| import argparse
 | |
| import glob
 | |
| import json
 | |
| import multiprocessing
 | |
| import os
 | |
| import re
 | |
| import shutil
 | |
| import subprocess
 | |
| import sys
 | |
| import tempfile
 | |
| import threading
 | |
| import traceback
 | |
| 
 | |
| try:
 | |
|     import yaml
 | |
| except ImportError:
 | |
|     yaml = None
 | |
| 
 | |
| is_py2 = sys.version[0] == "2"
 | |
| 
 | |
| if is_py2:
 | |
|     import Queue as queue
 | |
| else:
 | |
|     import queue
 | |
| 
 | |
| 
 | |
| def find_compilation_database(path, result="./"):
 | |
|     """Adjusts the directory until a compilation database is found."""
 | |
|     result = "./"
 | |
|     while not os.path.isfile(os.path.join(result, path)):
 | |
|         if os.path.realpath(result) == "/":
 | |
|             print("Warning: could not find compilation database.")
 | |
|             return None
 | |
|         result += "../"
 | |
|     return os.path.realpath(result)
 | |
| 
 | |
| 
 | |
| def make_absolute(f, directory):
 | |
|     """Convert a relative file path to an absolute file path."""
 | |
|     if os.path.isabs(f):
 | |
|         return f
 | |
|     return os.path.normpath(os.path.join(directory, f))
 | |
| 
 | |
| 
 | |
| def analysis_gitignore(path, filename=".gitignore"):
 | |
|     """Analysis gitignore file and return ignore file list"""
 | |
|     with open(path + "/" + filename, "r") as f:
 | |
|         lines = f.readlines()
 | |
|         ignore_file_list = []
 | |
|         for line in lines:
 | |
|             # Blank row
 | |
|             if line == "\n" or line == "\r\n":
 | |
|                 continue
 | |
| 
 | |
|             # explanatory note
 | |
|             line = line.replace("\n", "").strip()
 | |
|             if "#" in line:
 | |
|                 if not line.startswith("#"):
 | |
|                     ignore_file_list.append(line[: line.index("#")].replace(" ", ""))
 | |
|                 continue
 | |
| 
 | |
|             # TODO(gouzil): support more gitignore rules
 | |
|             if "*" in line:
 | |
|                 continue
 | |
| 
 | |
|             ignore_file_list.append(line.replace(" ", ""))
 | |
| 
 | |
|     return ignore_file_list
 | |
| 
 | |
| 
 | |
| def skip_check_file(database, build_path):
 | |
|     """Skip checking some files"""
 | |
|     skip_file_list = []
 | |
|     skip_file_list.append(".cu")
 | |
|     skip_file_list.append(os.path.join(os.getcwd(), build_path))
 | |
|     skip_file_list += analysis_gitignore(os.getcwd())
 | |
|     res_list = []
 | |
|     for entry in database:
 | |
|         write_in = True
 | |
|         for ignore_file in skip_file_list:
 | |
|             if ignore_file in entry["file"]:
 | |
|                 write_in = False
 | |
|                 break
 | |
|         if write_in:
 | |
|             res_list.append(entry)
 | |
| 
 | |
|     return res_list
 | |
| 
 | |
| 
 | |
| def get_tidy_invocation(
 | |
|     f,
 | |
|     clang_tidy_binary,
 | |
|     checks,
 | |
|     tmpdir,
 | |
|     build_path,
 | |
|     header_filter,
 | |
|     extra_arg,
 | |
|     extra_arg_before,
 | |
|     quiet,
 | |
|     config,
 | |
| ):
 | |
|     """Gets a command line for clang-tidy."""
 | |
|     start = [clang_tidy_binary]
 | |
|     if header_filter is not None:
 | |
|         start.append("-header-filter=" + header_filter)
 | |
|     if checks:
 | |
|         start.append("-checks=" + checks)
 | |
|     if tmpdir is not None:
 | |
|         start.append("-export-fixes")
 | |
|         # Get a temporary file. We immediately close the handle so clang-tidy can
 | |
|         # overwrite it.
 | |
|         (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
 | |
|         os.close(handle)
 | |
|         start.append(name)
 | |
|     for arg in extra_arg:
 | |
|         start.append(f"-extra-arg={arg}")
 | |
|     for arg in extra_arg_before:
 | |
|         start.append(f"-extra-arg-before={arg}")
 | |
|     start.append("-p=" + build_path)
 | |
|     if quiet:
 | |
|         start.append("-quiet")
 | |
|     if config:
 | |
|         start.append("-config=" + config)
 | |
|     start.append(f)
 | |
|     return start
 | |
| 
 | |
| 
 | |
| def merge_replacement_files(tmpdir, mergefile):
 | |
|     """Merge all replacement files in a directory into a single file"""
 | |
|     # The fixes suggested by clang-tidy >= 4.0.0 are given under
 | |
|     # the top level key 'Diagnostics' in the output yaml files
 | |
|     mergekey = "Diagnostics"
 | |
|     merged = []
 | |
|     for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
 | |
|         content = yaml.safe_load(open(replacefile, "r"))
 | |
|         if not content:
 | |
|             continue  # Skip empty files.
 | |
|         merged.extend(content.get(mergekey, []))
 | |
| 
 | |
|     if merged:
 | |
|         # MainSourceFile: The key is required by the definition inside
 | |
|         # include/clang/Tooling/ReplacementsYaml.h, but the value
 | |
|         # is actually never used inside clang-apply-replacements,
 | |
|         # so we set it to '' here.
 | |
|         output = {"MainSourceFile": "", mergekey: merged}
 | |
|         with open(mergefile, "w") as out:
 | |
|             yaml.safe_dump(output, out)
 | |
|     else:
 | |
|         # Empty the file:
 | |
|         open(mergefile, "w").close()
 | |
| 
 | |
| 
 | |
| def check_clang_apply_replacements_binary(args):
 | |
|     """Checks if invoking supplied clang-apply-replacements binary works."""
 | |
|     try:
 | |
|         subprocess.check_call([args.clang_apply_replacements_binary, "--version"])
 | |
|     except:
 | |
|         print(
 | |
|             "Unable to run clang-apply-replacements. Is clang-apply-replacements "
 | |
|             "binary correctly specified?",
 | |
|             file=sys.stderr,
 | |
|         )
 | |
|         traceback.print_exc()
 | |
|         sys.exit(1)
 | |
| 
 | |
| 
 | |
| def apply_fixes(args, tmpdir):
 | |
|     """Calls clang-apply-fixes on a given directory."""
 | |
|     invocation = [args.clang_apply_replacements_binary]
 | |
|     if args.format:
 | |
|         invocation.append("-format")
 | |
|     if args.style:
 | |
|         invocation.append("-style=" + args.style)
 | |
|     invocation.append(tmpdir)
 | |
|     subprocess.call(invocation)
 | |
| 
 | |
| 
 | |
| def run_tidy(args, tmpdir, build_path, queue, lock, failed_files):
 | |
|     """Takes filenames out of queue and runs clang-tidy on them."""
 | |
|     while True:
 | |
|         name = queue.get()
 | |
|         invocation = get_tidy_invocation(
 | |
|             name,
 | |
|             args.clang_tidy_binary,
 | |
|             args.checks,
 | |
|             tmpdir,
 | |
|             build_path,
 | |
|             args.header_filter,
 | |
|             args.extra_arg,
 | |
|             args.extra_arg_before,
 | |
|             args.quiet,
 | |
|             args.config,
 | |
|         )
 | |
| 
 | |
|         proc = subprocess.Popen(
 | |
|             invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 | |
|         )
 | |
|         output, err = proc.communicate()
 | |
|         if proc.returncode != 0:
 | |
|             failed_files.append(name)
 | |
|         with lock:
 | |
|             sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8"))
 | |
|             if len(err) > 0:
 | |
|                 sys.stdout.flush()
 | |
|                 sys.stderr.write(err.decode("utf-8"))
 | |
|         queue.task_done()
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     """Runs clang-tidy over all files in a compilation database."""
 | |
|     parser = argparse.ArgumentParser(
 | |
|         description="Runs clang-tidy over all files "
 | |
|         "in a compilation database. Requires "
 | |
|         "clang-tidy and clang-apply-replacements in "
 | |
|         "$PATH."
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-clang-tidy-binary",
 | |
|         metavar="PATH",
 | |
|         default="clang-tidy-15",
 | |
|         help="path to clang-tidy binary",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-clang-apply-replacements-binary",
 | |
|         metavar="PATH",
 | |
|         default="clang-apply-replacements-15",
 | |
|         help="path to clang-apply-replacements binary",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-checks",
 | |
|         default=None,
 | |
|         help="checks filter, when not specified, use clang-tidy " "default",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-config",
 | |
|         default=None,
 | |
|         help="Specifies a configuration in YAML/JSON format: "
 | |
|         "  -config=\"{Checks: '*', "
 | |
|         "                       CheckOptions: [{key: x, "
 | |
|         '                                       value: y}]}" '
 | |
|         "When the value is empty, clang-tidy will "
 | |
|         "attempt to find a file named .clang-tidy for "
 | |
|         "each source file in its parent directories.",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-header-filter",
 | |
|         default=None,
 | |
|         help="regular expression matching the names of the "
 | |
|         "headers to output diagnostics from. Diagnostics from "
 | |
|         "the main file of each translation unit are always "
 | |
|         "displayed.",
 | |
|     )
 | |
|     if yaml:
 | |
|         parser.add_argument(
 | |
|             "-export-fixes",
 | |
|             metavar="filename",
 | |
|             dest="export_fixes",
 | |
|             help="Create a yaml file to store suggested fixes in, "
 | |
|             "which can be applied with clang-apply-replacements.",
 | |
|         )
 | |
|     parser.add_argument(
 | |
|         "-j",
 | |
|         type=int,
 | |
|         default=0,
 | |
|         help="number of tidy instances to be run in parallel.",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "files",
 | |
|         nargs="*",
 | |
|         default=[".*"],
 | |
|         help="files to be processed (regex on path)",
 | |
|     )
 | |
|     parser.add_argument("-fix", action="store_true", help="apply fix-its")
 | |
|     parser.add_argument(
 | |
|         "-format",
 | |
|         action="store_true",
 | |
|         help="Reformat code " "after applying fixes",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-style",
 | |
|         default="file",
 | |
|         help="The style of reformat " "code after applying fixes",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-p",
 | |
|         dest="build_path",
 | |
|         help="Path used to read a compile command database.",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-extra-arg",
 | |
|         dest="extra_arg",
 | |
|         action="append",
 | |
|         default=[],
 | |
|         help="Additional argument to append to the compiler " "command line.",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-extra-arg-before",
 | |
|         dest="extra_arg_before",
 | |
|         action="append",
 | |
|         default=[],
 | |
|         help="Additional argument to prepend to the compiler " "command line.",
 | |
|     )
 | |
|     parser.add_argument(
 | |
|         "-quiet", action="store_true", help="Run clang-tidy in quiet mode"
 | |
|     )
 | |
|     args = parser.parse_args()
 | |
| 
 | |
|     db_path = "compile_commands.json"
 | |
| 
 | |
|     if args.build_path is not None:
 | |
|         build_path = args.build_path
 | |
|         if not os.path.isfile(os.path.join(build_path, db_path)):
 | |
|             print(
 | |
|                 f"Warning: could not find compilation database in {build_path}, skip clang-tidy check."
 | |
|             )
 | |
|             build_path = None
 | |
|     else:
 | |
|         # Find our database
 | |
|         build_path = find_compilation_database(db_path)
 | |
|     if build_path is None:
 | |
|         sys.exit(0)
 | |
| 
 | |
|     try:
 | |
|         invocation = [args.clang_tidy_binary, "-list-checks"]
 | |
|         invocation.append("-p=" + build_path)
 | |
|         if args.checks:
 | |
|             invocation.append("-checks=" + args.checks)
 | |
|         invocation.append("-")
 | |
|         if args.quiet:
 | |
|             # Even with -quiet we still want to check if we can call clang-tidy.
 | |
|             with open(os.devnull, "w") as dev_null:
 | |
|                 subprocess.check_call(invocation, stdout=dev_null)
 | |
|         else:
 | |
|             subprocess.check_call(invocation)
 | |
|     except:
 | |
|         print("Unable to run clang-tidy.", file=sys.stderr)
 | |
|         sys.exit(0)
 | |
| 
 | |
|     # Load the database and extract all files.
 | |
|     database = json.load(open(os.path.join(build_path, db_path)))
 | |
|     database = skip_check_file(database, build_path)
 | |
|     files = {make_absolute(entry["file"], entry["directory"]) for entry in database}
 | |
| 
 | |
|     max_task = args.j
 | |
|     if max_task == 0:
 | |
|         max_task = multiprocessing.cpu_count()
 | |
| 
 | |
|     tmpdir = None
 | |
|     if args.fix or (yaml and args.export_fixes):
 | |
|         check_clang_apply_replacements_binary(args)
 | |
|         tmpdir = tempfile.mkdtemp()
 | |
| 
 | |
|     # Build up a big regexy filter from all command line arguments.
 | |
|     file_name_re = re.compile("|".join(args.files))
 | |
| 
 | |
|     return_code = 0
 | |
|     try:
 | |
|         # Spin up a bunch of tidy-launching threads.
 | |
|         task_queue = queue.Queue(max_task)
 | |
|         # List of files with a non-zero return code.
 | |
|         failed_files = []
 | |
|         lock = threading.Lock()
 | |
|         for _ in range(max_task):
 | |
|             t = threading.Thread(
 | |
|                 target=run_tidy,
 | |
|                 args=(args, tmpdir, build_path, task_queue, lock, failed_files),
 | |
|             )
 | |
|             t.daemon = True
 | |
|             t.start()
 | |
| 
 | |
|         # Fill the queue with files.
 | |
|         for name in files:
 | |
|             if file_name_re.search(name):
 | |
|                 task_queue.put(name)
 | |
| 
 | |
|         # Wait for all threads to be done.
 | |
|         task_queue.join()
 | |
|         if len(failed_files):
 | |
|             return_code = 1
 | |
| 
 | |
|     except KeyboardInterrupt:
 | |
|         # This is a sad hack. Unfortunately subprocess goes
 | |
|         # bonkers with ctrl-c and we start forking merrily.
 | |
|         print("\nCtrl-C detected, goodbye.")
 | |
|         if tmpdir:
 | |
|             shutil.rmtree(tmpdir)
 | |
|         os.kill(0, 9)
 | |
| 
 | |
|     if yaml and args.export_fixes:
 | |
|         print("Writing fixes to " + args.export_fixes + " ...")
 | |
|         try:
 | |
|             merge_replacement_files(tmpdir, args.export_fixes)
 | |
|         except:
 | |
|             print("Error exporting fixes.\n", file=sys.stderr)
 | |
|             traceback.print_exc()
 | |
|             return_code = 1
 | |
| 
 | |
|     if args.fix:
 | |
|         print("Applying fixes ...")
 | |
|         try:
 | |
|             apply_fixes(args, tmpdir)
 | |
|         except:
 | |
|             print("Error applying fixes.\n", file=sys.stderr)
 | |
|             traceback.print_exc()
 | |
|             return_code = 1
 | |
| 
 | |
|     if tmpdir:
 | |
|         shutil.rmtree(tmpdir)
 | |
|     sys.exit(return_code)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     if os.getenv("SKIP_CLANG_TIDY_CHECK", "").lower() in [
 | |
|         "y",
 | |
|         "yes",
 | |
|         "t",
 | |
|         "true",
 | |
|         "on",
 | |
|         "1",
 | |
|     ]:
 | |
|         print(
 | |
|             "SKIP_CLANG_TIDY_CHECK is set, skip clang-tidy check.",
 | |
|             file=sys.stderr,
 | |
|         )
 | |
|         sys.exit(0)
 | |
| 
 | |
|     target_version = "15.0.2"
 | |
|     try:
 | |
|         out = subprocess.check_output(["clang-tidy", "--version"], shell=True)
 | |
|         version = out.decode("utf-8")
 | |
|         if version.find(target_version) == -1:
 | |
|             print(
 | |
|                 f"clang-tidy version == {target_version} not found, attempting auto-install...",
 | |
|                 file=sys.stderr,
 | |
|             )
 | |
|             subprocess.check_output(
 | |
|                 'pip install --no-cache clang-tidy=="15.0.2.1"',
 | |
|                 shell=True,
 | |
|             )
 | |
|     except:
 | |
|         print("clang-tidy not found, attempting auto-install...", file=sys.stderr)
 | |
|         subprocess.check_output(
 | |
|             'pip install --no-cache clang-tidy=="15.0.2.1"',
 | |
|             shell=True,
 | |
|         )
 | |
|     main()
 | 
