forked from blender/blender
269 lines
7.8 KiB
Python
Executable File
269 lines
7.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
"""
|
|
This script runs clang-format on multiple files/directories.
|
|
|
|
While it can be called directly, you may prefer to run this from Blender's root directory with the command:
|
|
|
|
make format
|
|
|
|
"""
|
|
|
|
import argparse
|
|
import multiprocessing
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
|
|
from typing import (
|
|
List,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
)
|
|
|
|
VERSION_MIN = (8, 0, 0)
|
|
VERSION_MAX_RECOMMENDED = (12, 0, 0)
|
|
CLANG_FORMAT_CMD = "clang-format"
|
|
|
|
BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
os.chdir(BASE_DIR)
|
|
|
|
|
|
extensions = (
|
|
".c", ".cc", ".cpp", ".cxx",
|
|
".h", ".hh", ".hpp", ".hxx",
|
|
".m", ".mm",
|
|
".osl", ".glsl",
|
|
)
|
|
|
|
extensions_only_retab = (
|
|
".cmake",
|
|
"CMakeLists.txt",
|
|
".sh",
|
|
)
|
|
|
|
ignore_files = {
|
|
"intern/cycles/render/sobol.cpp", # Too heavy for clang-format
|
|
}
|
|
|
|
|
|
def compute_paths(paths: List[str], use_default_paths: bool) -> List[str]:
|
|
# Optionally pass in files to operate on.
|
|
if use_default_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/libmv",
|
|
"intern/locale",
|
|
"intern/memutil",
|
|
"intern/mikktspace",
|
|
"intern/opencolorio",
|
|
"intern/opensubdiv",
|
|
"intern/openvdb",
|
|
"intern/rigidbody",
|
|
"intern/utfconv",
|
|
"source",
|
|
"tests/gtests",
|
|
]
|
|
else:
|
|
# Filter out files, this is only done so this utility wont print that it's
|
|
# "Operating" on files that will be filtered out later on.
|
|
paths = [
|
|
f for f in paths
|
|
if os.path.isdir(f) or (os.path.isfile(f) and f.endswith(extensions))
|
|
]
|
|
|
|
if os.sep != "/":
|
|
paths = [f.replace("/", os.sep) for f in paths]
|
|
return paths
|
|
|
|
|
|
def source_files_from_git(paths: Sequence[str], changed_only: bool) -> List[str]:
|
|
if changed_only:
|
|
cmd = ("git", "diff", "HEAD", "--name-only", "-z", "--", *paths)
|
|
else:
|
|
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: Sequence[str]) -> None:
|
|
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: str) -> str:
|
|
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_ensure_version() -> Optional[Tuple[int, int, int]]:
|
|
global CLANG_FORMAT_CMD
|
|
clang_format_cmd = None
|
|
version_output = ""
|
|
for i in range(2, -1, -1):
|
|
clang_format_cmd = (
|
|
"clang-format-" + (".".join(["%d"] * i) % VERSION_MIN[:i])
|
|
if i > 0 else
|
|
"clang-format"
|
|
)
|
|
try:
|
|
version_output = subprocess.check_output((clang_format_cmd, "-version")).decode('utf-8')
|
|
except FileNotFoundError:
|
|
continue
|
|
CLANG_FORMAT_CMD = clang_format_cmd
|
|
break
|
|
version: Optional[str] = next(iter(v for v in version_output.split() if v[0].isdigit()), None)
|
|
if version is None:
|
|
return None
|
|
|
|
version = version.split("-")[0]
|
|
# Ensure exactly 3 numbers.
|
|
version_num: Tuple[int, int, int] = (tuple(int(n) for n in version.split(".")) + (0, 0, 0))[:3] # type: ignore
|
|
print("Using %s (%d.%d.%d)..." % (CLANG_FORMAT_CMD, version_num[0], version_num[1], version_num[2]))
|
|
return version_num
|
|
|
|
|
|
def clang_format_file(files: List[str]) -> bytes:
|
|
cmd = [
|
|
CLANG_FORMAT_CMD,
|
|
# Update the files in-place.
|
|
"-i",
|
|
# Shows the list of processed files.
|
|
"-verbose",
|
|
] + files
|
|
return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
|
|
|
|
def clang_print_output(output: bytes) -> None:
|
|
print(output.decode('utf8', errors='ignore').strip())
|
|
|
|
|
|
def clang_format(files: List[str]) -> None:
|
|
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() -> argparse.ArgumentParser:
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Format C/C++/GLSL & Objective-C source code.",
|
|
epilog=__doc__,
|
|
# Don't re-wrap text, keep newlines & indentation.
|
|
formatter_class=argparse.RawTextHelpFormatter,
|
|
|
|
)
|
|
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(
|
|
"--changed-only",
|
|
dest="changed_only",
|
|
default=False,
|
|
action='store_true',
|
|
help=(
|
|
"Format only edited files, including the staged ones. "
|
|
"Using this with \"paths\" will pick the edited files lying on those paths. "
|
|
"(default=False)"
|
|
),
|
|
required=False,
|
|
)
|
|
parser.add_argument(
|
|
"paths",
|
|
nargs=argparse.REMAINDER,
|
|
help="All trailing arguments are treated as paths.",
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def main() -> None:
|
|
version = clang_format_ensure_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)
|
|
if version > VERSION_MAX_RECOMMENDED:
|
|
print(
|
|
"WARNING: Version of clang-format is too recent:",
|
|
version, ">", VERSION_MAX_RECOMMENDED,
|
|
)
|
|
print(
|
|
"You may want to install clang-format-%d.%d, "
|
|
"or use the precompiled libs repository." %
|
|
(VERSION_MAX_RECOMMENDED[0], VERSION_MAX_RECOMMENDED[1]),
|
|
)
|
|
|
|
args = argparse_create().parse_args()
|
|
|
|
use_default_paths = not (bool(args.paths) or bool(args.changed_only))
|
|
|
|
paths = compute_paths(args.paths, use_default_paths)
|
|
print("Operating on:" + (" (%d changed paths)" % len(paths) if args.changed_only else ""))
|
|
for p in paths:
|
|
print(" ", p)
|
|
|
|
files = [
|
|
f for f in source_files_from_git(paths, args.changed_only)
|
|
if f.endswith(extensions)
|
|
if f not in ignore_files
|
|
]
|
|
|
|
# Always operate on all CMAKE files (when expanding tabs and no paths given).
|
|
files_retab = [
|
|
f for f in source_files_from_git((".",) if use_default_paths else paths, args.changed_only)
|
|
if f.endswith(extensions_only_retab)
|
|
if f not in ignore_files
|
|
]
|
|
|
|
if args.expand_tabs:
|
|
convert_tabs_to_spaces(files + files_retab)
|
|
clang_format(files)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|