diff --git a/utils/clang_format_paths.py b/utils/clang_format_paths.py new file mode 100755 index 0000000000000000000000000000000000000000..06e813f1e8e84bc38f72c49f9673351a63a6e9ff --- /dev/null +++ b/utils/clang_format_paths.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 + +import multiprocessing +import os +import sys +import subprocess + +CLANG_FORMAT_CMD = "clang-format" +VERSION_MIN = (6, 0, 0) + + +extensions = ( + ".c", ".cc", ".cpp", ".cxx", + ".h", ".hh", ".hpp", ".hxx", + ".m", ".mm", + ".osl", ".glsl", +) + +ignore_files = { + "intern/cycles/render/sobol.cpp", # Too heavy for clang-format +} + + +def compute_paths(paths): + # Optionally pass in files to operate on. + if not paths: + paths = ( + "intern/atomic", + "intern/audaspace", + "intern/clog", + "intern/cycles", + "intern/dualcon", + "intern/eigen", + "intern/ffmpeg", + "intern/ghost", + "intern/glew-mx", + "intern/guardedalloc", + "intern/iksolver", + "intern/locale", + "intern/memutil", + "intern/mikktspace", + "intern/opencolorio", + "intern/openvdb", + "intern/rigidbody", + "intern/string", + "intern/utfconv", + "source", + "tests/gtests", + ) + + if os.sep != "/": + paths = [f.replace("/", os.sep) for f in paths] + return paths + + +def source_files_from_git(paths): + cmd = ("git", "ls-tree", "-r", "HEAD", *paths, "--name-only", "-z") + files = subprocess.check_output(cmd).split(b'\0') + return [f.decode('ascii') for f in files] + + +def convert_tabs_to_spaces(files): + for f in files: + print("TabExpand", f) + with open(f, 'r', encoding="utf-8") as fh: + data = fh.read() + if False: + # Simple 4 space + data = data.expandtabs(4) + else: + # Complex 2 space + # because some comments have tabs for alignment. + def handle(l): + ls = l.lstrip("\t") + d = len(l) - len(ls) + if d != 0: + return (" " * d) + ls.expandtabs(4) + else: + return l.expandtabs(4) + + lines = data.splitlines(keepends=True) + lines = [handle(l) for l in lines] + data = "".join(lines) + with open(f, 'w', encoding="utf-8") as fh: + fh.write(data) + + +def clang_format_version(): + version_output = subprocess.check_output((CLANG_FORMAT_CMD, "-version")).decode('utf-8') + version = next(iter(v for v in version_output.split() if v[0].isdigit()), None) + if version is not None: + version = version.split("-")[0] + version = tuple(int(n) for n in version.split(".")) + return version + + +def clang_format_file(files): + cmd = ["clang-format", "-i", "-verbose"] + files + return subprocess.check_output(cmd, stderr=subprocess.STDOUT) + + +def clang_print_output(output): + print(output.decode('utf8', errors='ignore').strip()) + + +def clang_format(files): + pool = multiprocessing.Pool() + + # Process in chunks to reduce overhead of starting processes. + cpu_count = multiprocessing.cpu_count() + chunk_size = min(max(len(files) // cpu_count // 2, 1), 32) + for i in range(0, len(files), chunk_size): + files_chunk = files[i:i+chunk_size]; + pool.apply_async(clang_format_file, args=[files_chunk], callback=clang_print_output) + + pool.close() + pool.join() + +def argparse_create(): + import argparse + + # When --help or no args are given, print this help + usage_text = "Format source code" + epilog = "This script runs clang-format on multiple files/directories" + parser = argparse.ArgumentParser(description=usage_text, epilog=epilog) + parser.add_argument( + "--expand-tabs", + dest="expand_tabs", + default=False, + action='store_true', + help="Run a pre-pass that expands tabs " + "(default=False)", + required=False, + ) + parser.add_argument( + "paths", + nargs=argparse.REMAINDER, + help="All trailing arguments are treated as paths." + ) + + return parser + + +def main(): + version = clang_format_version() + if version is None: + print("Unable to detect 'clang-format -version'") + sys.exit(1) + if version < VERSION_MIN: + print("Version of clang-format is too old:", version, "<", VERSION_MIN) + sys.exit(1) + + args = argparse_create().parse_args() + + paths = compute_paths(args.paths) + print("Operating on:") + for p in paths: + print(" ", p) + + files = [ + f for f in source_files_from_git(paths) + if f.endswith(extensions) + if f not in ignore_files + ] + + if args.expand_tabs: + convert_tabs_to_spaces(files) + clang_format(files) + + +if __name__ == "__main__": + main()