|
| 1 | +""" |
| 2 | +This script will call clang-format. |
| 3 | +
|
| 4 | +Call example: run-clang-format.py - Parallel clang-format runner |
| 5 | +
|
| 6 | +Based on run-clang-tidy.py, which is part of the LLVM Project, under the |
| 7 | +Apache License v2.0 with LLVM Exceptions. |
| 8 | +See https://llvm.org/LICENSE.txt for license information. |
| 9 | +SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 10 | +""" |
| 11 | + |
| 12 | +import argparse |
| 13 | +import json |
| 14 | +import multiprocessing |
| 15 | +import os |
| 16 | +import subprocess |
| 17 | +import sys |
| 18 | +import threading |
| 19 | +import queue as queue |
| 20 | + |
| 21 | + |
| 22 | +def find_compilation_database(path): |
| 23 | + """Adjust the directory until a compilation database is found.""" |
| 24 | + result = './' |
| 25 | + while not os.path.isfile(os.path.join(result, path)): |
| 26 | + if os.path.realpath(result) == '/': |
| 27 | + print('Error: could not find compilation database.') |
| 28 | + sys.exit(1) |
| 29 | + result += '../' |
| 30 | + return os.path.realpath(result) |
| 31 | + |
| 32 | + |
| 33 | +def make_absolute(f, directory): |
| 34 | + """Create a absolute path from given parameters.""" |
| 35 | + if os.path.isabs(f): |
| 36 | + return f |
| 37 | + return os.path.normpath(os.path.join(directory, f)) |
| 38 | + |
| 39 | + |
| 40 | +def get_format_invocation(f, clang_format_binary, fix, warnings_as_errors, quiet): |
| 41 | + """Get a command line for clang-format.""" |
| 42 | + start = [clang_format_binary] |
| 43 | + if fix: |
| 44 | + start.append('-i') |
| 45 | + else: |
| 46 | + start.append('--dry-run') |
| 47 | + if warnings_as_errors: |
| 48 | + start.append('--Werror') |
| 49 | + start.append(f) |
| 50 | + return start |
| 51 | + |
| 52 | + |
| 53 | +def run_format(args, _, queue, lock, failed_files): |
| 54 | + """Take filenames out of queue and runs clang-format on them.""" |
| 55 | + while True: |
| 56 | + name = queue.get() |
| 57 | + invocation = get_format_invocation(name, args.clang_format_binary, args.fix, args.warnings_as_errors, |
| 58 | + args.quiet) |
| 59 | + |
| 60 | + proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 61 | + output, err = proc.communicate() |
| 62 | + if proc.returncode != 0: |
| 63 | + failed_files.append(name) |
| 64 | + with lock: |
| 65 | + sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8')) |
| 66 | + if len(err) > 0: |
| 67 | + sys.stdout.flush() |
| 68 | + sys.stderr.write(err.decode('utf-8')) |
| 69 | + queue.task_done() |
| 70 | + |
| 71 | + |
| 72 | +if __name__ == '__main__': |
| 73 | + parser = argparse.ArgumentParser(description='Runs clang-format over all files ' |
| 74 | + 'in a compilation database. Requires ' |
| 75 | + 'clang-format in $PATH.') |
| 76 | + parser.add_argument('-clang-format-binary', metavar='PATH', default='clang-format', |
| 77 | + help='path to clang-format binary') |
| 78 | + parser.add_argument('-p', dest='build_path', help='Path used to read a compile command database.') |
| 79 | + parser.add_argument('-j', type=int, default=0, help='number of tidy instances to be run in parallel.') |
| 80 | + parser.add_argument('-fix', action='store_true', help='reformat files') |
| 81 | + parser.add_argument('-warnings-as-errors', action='store_true', |
| 82 | + help='Let the clang-tidy process return != 0 if a check failed.') |
| 83 | + parser.add_argument('-quiet', action='store_true', help='Run clang-format in quiet mode') |
| 84 | + args = parser.parse_args() |
| 85 | + |
| 86 | + db_path = 'compile_commands.json' |
| 87 | + |
| 88 | + if args.build_path is not None: |
| 89 | + build_path = args.build_path |
| 90 | + else: |
| 91 | + # Find our database |
| 92 | + build_path = find_compilation_database(db_path) |
| 93 | + |
| 94 | + try: |
| 95 | + with open(os.devnull, 'w') as dev_null: |
| 96 | + subprocess.check_call([args.clang_format_binary, '--dump-config'], stdout=dev_null) |
| 97 | + except Exception as ex: |
| 98 | + print(f'Unable to run clang-format. {ex} - {sys.stderr}') |
| 99 | + sys.exit(1) |
| 100 | + |
| 101 | + # Load the database and extract all files. |
| 102 | + database = json.load(open(os.path.join(build_path, db_path))) |
| 103 | + files = [make_absolute(entry['file'], entry['directory']) for entry in database] |
| 104 | + |
| 105 | + max_task = args.j |
| 106 | + if max_task == 0: |
| 107 | + max_task = multiprocessing.cpu_count() |
| 108 | + |
| 109 | + return_code = 0 |
| 110 | + try: |
| 111 | + # Spin up a bunch of format-launching threads. |
| 112 | + task_queue = queue.Queue(max_task) |
| 113 | + # List of files with a non-zero return code. |
| 114 | + failed_files = [] |
| 115 | + lock = threading.Lock() |
| 116 | + for _ in range(max_task): |
| 117 | + t = threading.Thread(target=run_format, args=(args, build_path, task_queue, lock, failed_files)) |
| 118 | + t.daemon = True |
| 119 | + t.start() |
| 120 | + |
| 121 | + # Fill the queue with files. |
| 122 | + for name in files: |
| 123 | + task_queue.put(name) |
| 124 | + |
| 125 | + # Wait for all threads to be done. |
| 126 | + task_queue.join() |
| 127 | + if len(failed_files): |
| 128 | + return_code = 1 |
| 129 | + |
| 130 | + except KeyboardInterrupt: |
| 131 | + # This is a sad hack. Unfortunately subprocess goes |
| 132 | + # bonkers with ctrl-c and we start forking merrily. |
| 133 | + print('\nCtrl-C detected, goodbye.') |
| 134 | + os.kill(0, 9) |
| 135 | + |
| 136 | + sys.exit(return_code) |
0 commit comments