Files
UnrealEngine/Engine/Extras/clang-format/perforce-clang-format-diff.py
2025-05-18 13:04:45 +08:00

238 lines
8.1 KiB
Python

from dataclasses import dataclass
import argparse
import os
import platform
import re
import shutil
import subprocess
import sys
activate_print_verbose = False
def print_verbose(*args, **kwargs):
if activate_print_verbose:
print("VERBOSE:", *args, **kwargs)
def print_warning(*args, **kwargs):
print("WARNING:", *args, **kwargs)
def decode_string(string):
if type(string) is str:
return string
try:
result = string.decode("utf-8")
except UnicodeDecodeError:
try:
result = string.decode("cp1252")
except UnicodeDecodeError:
print_warning("Failed to decode so returning original string \"{0}\"".format(string))
return string
return result
def run_subprocess(command, **kwargs):
environment = os.environ.copy()
# Add additional paths on the Mac so it can find p4 etc.
if platform.system() == "Darwin":
additional_paths = ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin"]
new_path = os.environ.get("PATH", "")
for path in additional_paths:
new_path = os.pathsep.join([new_path, path])
environment = environment | {"PATH": new_path}
return subprocess.run(command, capture_output=True, env=environment, **kwargs)
@dataclass
class P4_ztag():
file_path: str = None
depot_file: str = None
is_add: bool = False
is_open: bool = True
def p4_ztag(path):
command = ["p4", "-ztag", "fstat", "-Ro", path]
result = run_subprocess(command)
stdout_string = decode_string(result.stdout)
stderr_string = decode_string(result.stderr)
to_return = P4_ztag(file_path = path)
if "not opened on this client" in stderr_string:
to_return.is_open = False
return to_return
lines = stdout_string.splitlines()
depot_file_regex = re.compile(r'^\.\.\. depotFile (.*)')
type_regex = re.compile(r'^\.\.\. type (.*)')
action_regex = re.compile(r'^\.\.\. action (.*)')
for line in lines:
if match := depot_file_regex.match(line):
to_return.depot_file = match.group(1)
if match := type_regex.match(line):
file_type = match.group(1)
if file_type.strip() != "text":
return None
if match := action_regex.match(line):
action = match.group(1)
if action.strip() == "add":
to_return.is_add = True
return to_return
def clang_format(exe_path, config_path, file_path, verify=False):
"""Returns True if the diff needed formatting, False otherwise."""
command = [exe_path, "-style=file:%s" % config_path, "-i", file_path]
if verify:
command.append("--dry-run")
command.append("-Werror")
result = run_subprocess(command)
if result.returncode == 1:
return True
if result.returncode != 0:
print("Non-zero return code {0} from {1}".format(result.returncode, " ".join(command)))
print("stderr:", result.stderr)
return
print(decode_string(result.stdout))
return False
def p4_diff(path):
command = ["p4", "diff", "-du0", path]
result = run_subprocess(command)
if result.returncode != 0:
raise Exception("p4 diff failed: %s" % result.stderr)
stdout_string = decode_string(result.stdout)
return stdout_string
def p4_open(path):
command = ["p4", "open", path]
result = run_subprocess(command)
if result.returncode != 0:
raise Exception("p4 open failed: %s" % result.stderr)
def clang_format_diff(clang_format_diff_path, clang_format_path, config_path, diff, verify=False):
"""Returns True if the diff needed formatting, False otherwise."""
# Figure out which python executable to call.
python_command = None
if shutil.which("python") is not None:
python_command = "python"
elif shutil.which("python3") is not None:
python_command = "python3"
else:
raise Exception("Could not run clang-format-diff.py: could not find \"python\" or \"python3\" executable to run it with.")
print_verbose("Using python command \"%s\"." % python_command)
command = [python_command, clang_format_diff_path, "-binary=%s" % clang_format_path, "-style=file:%s" % config_path, "-v"]
if not verify:
# Write the formatted code back to the files being formatted.
command.append("-i")
result = run_subprocess(command, text=True, input=diff)
# clang-format-diff returns 1 when the file needs to be formatted.
if result.returncode == 1:
return True
elif result.returncode != 0:
print("Non-zero return code {0} from {1}".format(result.returncode, " ".join(command)))
print("stderr:", result.stderr)
return False
def main():
args_parser = argparse.ArgumentParser(
prog="perforce-clang-format",
description="Uses Perforce to run clang-format on the diff of files you've changed",
)
args_parser.add_argument("paths", nargs="*")
args_parser.add_argument("-v", "--verbose", action="store_true")
args_parser.add_argument("--clang-format-path")
args_parser.add_argument("--clang-format-config-path")
args_parser.add_argument("--clang-format-diff-path")
args_parser.add_argument("--verify", action="store_true")
args = args_parser.parse_args()
global activate_print_verbose
activate_print_verbose = args.verbose
print_verbose("args.verify=", args.verify)
print_verbose("args.paths=", args.paths)
script_dir = os.path.dirname(os.path.realpath(__file__))
clang_format_path = args.clang_format_path
if clang_format_path is None:
if platform.system() == "Darwin":
clang_format_path = os.path.join(script_dir, "Mac-arm64", "clang-format")
elif platform.system() == "Windows":
clang_format_path = os.path.join(script_dir, "Win64", "clang-format.exe")
elif platform.system() == "Linux":
raise Exception("Linux is not yet supported.")
else:
# Support the old clang-format.exe location for now.
clang_format_path = os.path.join(script_dir, "clang-format.exe")
config_path = args.clang_format_config_path
if config_path is None:
config_path = os.path.join(script_dir, "experimental.clang-format")
clang_format_diff_path = args.clang_format_diff_path
if clang_format_diff_path is None:
clang_format_diff_path = os.path.join(script_dir, "clang-format-diff.py")
files_that_need_formatting = []
for path in args.paths:
ztag = p4_ztag(path)
if ztag is None:
continue
if ztag.is_add:
if args.verify:
print_verbose("Checking formatting of new file \"%s\"." % ztag.file_path)
else:
print_verbose("Fully formatting new file \"%s\"." % ztag.file_path)
if clang_format(clang_format_path, config_path, ztag.file_path, args.verify):
if args.verify:
files_that_need_formatting.append(ztag.file_path)
elif not ztag.is_open:
if not args.verify:
print_verbose("Opening unopened file \"%s\"." % ztag.file_path)
p4_open(ztag.file_path)
diff = p4_diff(ztag.file_path)
if args.verify:
print_verbose("Checking formatting of diff of file \"%s\"." % ztag.file_path)
else:
print_verbose("Formatting diff of file \"%s\"." % ztag.file_path)
if clang_format_diff(clang_format_diff_path, clang_format_path, config_path, diff, args.verify):
if args.verify:
files_that_need_formatting.append(ztag.file_path)
else:
diff = p4_diff(ztag.file_path)
print_verbose("Formatting diff of file \"%s\"." % ztag.file_path)
if clang_format_diff(clang_format_diff_path, clang_format_path, config_path, diff, args.verify):
if args.verify:
files_that_need_formatting.append(ztag.file_path)
if len(files_that_need_formatting) > 0:
for file_path in files_that_need_formatting:
print("File needs formatting: \"%s\"." % file_path)
sys.exit(1)
if __name__ == "__main__":
main()