2021-11-23 14:31:59 +01:00
|
|
|
#!/usr/bin/env python3
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
# author: Ole Schuett
|
|
|
|
|
|
|
|
|
|
import ast
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
from time import time, sleep
|
|
|
|
|
import re
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import concurrent.futures
|
|
|
|
|
from subprocess import PIPE, STDOUT
|
|
|
|
|
import subprocess
|
|
|
|
|
import traceback
|
|
|
|
|
from urllib.request import urlopen, Request
|
2022-07-16 13:47:33 +02:00
|
|
|
from http.client import HTTPResponse
|
2020-08-18 18:27:16 +02:00
|
|
|
from urllib.error import HTTPError
|
|
|
|
|
import uuid
|
|
|
|
|
from difflib import unified_diff
|
2020-08-21 00:30:35 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
import shutil
|
2022-07-16 13:47:33 +02:00
|
|
|
from typing import cast, Iterator
|
2020-08-18 18:27:16 +02:00
|
|
|
|
2020-08-21 00:30:35 +02:00
|
|
|
SCRATCH_DIR = Path("./obj/precommit")
|
|
|
|
|
CACHE_FILE = SCRATCH_DIR / "cache.json"
|
2020-08-18 18:27:16 +02:00
|
|
|
SERVER = os.environ.get("CP2K_PRECOMMIT_SERVER", "https://precommit.cp2k.org")
|
|
|
|
|
|
2023-05-02 11:45:13 +02:00
|
|
|
|
2020-08-18 18:27:16 +02:00
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def main() -> None:
|
2020-08-18 18:27:16 +02:00
|
|
|
# Parse command line arguments.
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description="Check source code for formatting and linter problems."
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"-a",
|
|
|
|
|
"--no-cache",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="ignore the cache and check all files",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"-m",
|
|
|
|
|
"--allow-modifications",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="allow the tools to modify files",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"-j",
|
|
|
|
|
"--num_workers",
|
|
|
|
|
type=int,
|
2022-07-16 13:47:33 +02:00
|
|
|
default=min(16, (os.cpu_count() or 0) + 2),
|
2020-08-18 18:27:16 +02:00
|
|
|
help="number of parallel workers",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--progressbar-wait",
|
2020-08-21 00:30:35 +02:00
|
|
|
metavar="SECONDS",
|
2020-08-18 18:27:16 +02:00
|
|
|
type=int,
|
|
|
|
|
default=1,
|
|
|
|
|
help="number seconds in between progressbar updates",
|
|
|
|
|
)
|
2020-08-21 00:30:35 +02:00
|
|
|
parser.add_argument("files", metavar="FILE", nargs="*", help="files to process")
|
2020-08-18 18:27:16 +02:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
# Say hello to the server.
|
|
|
|
|
print(
|
|
|
|
|
f"Running precommit checks using {args.num_workers} workers and server: {SERVER}"
|
|
|
|
|
)
|
|
|
|
|
server_hello = urlopen(Request(SERVER + "/"), timeout=10).read().decode("utf8")
|
|
|
|
|
assert server_hello.startswith("cp2k precommit server")
|
|
|
|
|
|
2020-08-21 00:30:35 +02:00
|
|
|
# Store candidate before changing base directory and creating scratch dir.
|
2021-03-11 13:07:22 +01:00
|
|
|
file_list = [os.path.abspath(fn) for fn in args.files]
|
2020-08-30 00:25:13 +02:00
|
|
|
base_dir = Path(__file__).resolve().parent.parent.parent
|
2020-08-18 18:27:16 +02:00
|
|
|
os.chdir(base_dir)
|
2020-08-21 00:30:35 +02:00
|
|
|
SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# Collect candidate files.
|
2021-03-11 13:07:22 +01:00
|
|
|
if not file_list:
|
2020-08-21 00:30:35 +02:00
|
|
|
sys.stdout.write("Searching for files...\r")
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
for root, dirs, files in os.walk("."):
|
|
|
|
|
if root.startswith("./tools/toolchain/build"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./tools/toolchain/install"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./tools/prettify/fprettify"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./tools/build_utils/fypp"):
|
|
|
|
|
continue
|
2021-03-11 13:07:22 +01:00
|
|
|
if root.startswith("./tools/autotools"):
|
|
|
|
|
continue
|
2021-11-10 22:10:06 +01:00
|
|
|
if root.startswith("./tools/minimax_tools/1_xData"):
|
|
|
|
|
continue
|
2024-03-28 21:07:23 +01:00
|
|
|
if root.startswith("./tools/fedora"):
|
|
|
|
|
continue
|
2021-03-11 13:07:22 +01:00
|
|
|
if root.startswith("./data/DFTB/scc"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./arch"):
|
2020-08-21 00:30:35 +02:00
|
|
|
continue
|
2020-09-01 00:40:07 +02:00
|
|
|
if root.startswith("./doxygen"):
|
|
|
|
|
continue
|
2023-06-06 22:20:50 +02:00
|
|
|
if root.startswith("./docs/_build"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./docs/CP2K_INPUT"):
|
|
|
|
|
continue
|
2020-08-21 00:30:35 +02:00
|
|
|
if root.startswith("./exts"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./obj"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./lib"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./exe"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./regtesting"):
|
|
|
|
|
continue
|
|
|
|
|
if root.startswith("./.git"):
|
|
|
|
|
continue
|
2021-11-10 22:10:06 +01:00
|
|
|
if "/.mypy_cache/" in root:
|
|
|
|
|
continue
|
2021-03-11 13:07:22 +01:00
|
|
|
file_list += [os.path.join(root, fn) for fn in files]
|
2020-08-21 00:30:35 +02:00
|
|
|
|
2022-07-16 13:05:53 +02:00
|
|
|
# Filter symlinks, backup copies, logs, and hidden files.
|
2021-03-11 13:07:22 +01:00
|
|
|
file_list = [fn for fn in file_list if not os.path.islink(fn)]
|
|
|
|
|
file_list = [fn for fn in file_list if not fn[-1] in ("~", "#")]
|
2022-07-16 13:05:53 +02:00
|
|
|
file_list = [fn for fn in file_list if not fn.endswith(".log")]
|
2021-03-11 13:07:22 +01:00
|
|
|
file_list = [fn for fn in file_list if not os.path.basename(fn).startswith(".")]
|
|
|
|
|
|
|
|
|
|
# Sort files by size as larger ones will take longer to process.
|
2020-08-21 00:30:35 +02:00
|
|
|
file_list.sort(reverse=True, key=lambda fn: os.path.getsize(fn))
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
# Load cache.
|
2020-08-21 00:30:35 +02:00
|
|
|
should_load_cache = CACHE_FILE.exists() and not args.no_cache
|
|
|
|
|
cache = json.loads(CACHE_FILE.read_text()) if should_load_cache else {}
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
# Launch async processing of files.
|
|
|
|
|
futures = {}
|
|
|
|
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=args.num_workers)
|
|
|
|
|
for fn in file_list:
|
2020-08-21 00:30:35 +02:00
|
|
|
if os.path.getmtime(fn) != cache.get(fn, -1):
|
2020-08-18 18:27:16 +02:00
|
|
|
futures[fn] = executor.submit(process_file, fn, args.allow_modifications)
|
|
|
|
|
num_skipped = len(file_list) - len(futures)
|
|
|
|
|
|
|
|
|
|
# Continuously update progressbar, save cache file, and print errors.
|
|
|
|
|
failed_files = set()
|
|
|
|
|
while True:
|
|
|
|
|
num_done = num_skipped
|
|
|
|
|
for fn, f in futures.items():
|
|
|
|
|
if f.done():
|
|
|
|
|
num_done += 1
|
|
|
|
|
if not f.exception():
|
2020-08-21 00:30:35 +02:00
|
|
|
cache[fn] = os.path.getmtime(fn)
|
2020-08-18 18:27:16 +02:00
|
|
|
elif fn not in failed_files:
|
|
|
|
|
failed_files.add(fn)
|
|
|
|
|
print_box(fn, str(f.exception()))
|
2020-08-21 00:30:35 +02:00
|
|
|
CACHE_FILE.write_text(json.dumps(cache))
|
2020-08-18 18:27:16 +02:00
|
|
|
progressbar = "=" * int(60 * num_done / len(file_list))
|
|
|
|
|
sys.stdout.write(
|
|
|
|
|
f"[{progressbar:60s}] {num_done} / {len(file_list)} files processed\r"
|
|
|
|
|
)
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
if num_done == len(file_list) or len(failed_files) >= 10:
|
|
|
|
|
executor.shutdown(wait=False)
|
|
|
|
|
break
|
|
|
|
|
sleep(args.progressbar_wait)
|
|
|
|
|
|
|
|
|
|
# Print final message.
|
|
|
|
|
print(
|
|
|
|
|
f"Summary: Found {len(file_list)}, "
|
|
|
|
|
f"skipped {num_skipped}, "
|
|
|
|
|
f"checked {num_done - num_skipped}, "
|
|
|
|
|
f"and failed {len(failed_files)} files." + (" " * 50)
|
|
|
|
|
)
|
|
|
|
|
print("Status: " + ("FAILED" if failed_files else "OK"))
|
|
|
|
|
sys.exit(len(failed_files))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def print_box(fn: str, message: str) -> None:
|
2020-08-18 18:27:16 +02:00
|
|
|
print("+" + "-" * 160 + "+")
|
|
|
|
|
print(f"| {fn:^158s} |")
|
|
|
|
|
print("+" + "-" * 160 + "+")
|
|
|
|
|
for line in message.strip().split("\n"):
|
|
|
|
|
print(f"| {line:<158s} |")
|
|
|
|
|
print("+" + "-" * 160 + "+\n\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def process_file(fn: str, allow_modifications: bool) -> None:
|
2020-08-21 00:30:35 +02:00
|
|
|
# Make a backup copy.
|
|
|
|
|
orig_content = Path(fn).read_bytes()
|
2020-08-24 23:11:31 +02:00
|
|
|
bak_fn = SCRATCH_DIR / f"{Path(fn).name}_{time()}.bak"
|
2020-08-21 00:30:35 +02:00
|
|
|
shutil.copy2(fn, bak_fn)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
if re.match(r".*\.(F|fypp)$", fn):
|
|
|
|
|
run_local_tool("./tools/doxify/doxify.sh", fn)
|
|
|
|
|
run_prettify(fn)
|
|
|
|
|
|
2024-03-15 16:41:35 +01:00
|
|
|
if re.match(r".*\.(c|cu|cl|h)$", fn):
|
2020-08-18 18:27:16 +02:00
|
|
|
run_remote_tool("clangformat", fn)
|
|
|
|
|
|
2022-03-20 16:56:21 +01:00
|
|
|
if re.match(r".*\.(cc|cpp|cxx|hcc|hpp|hxx)$", fn):
|
2022-11-29 13:49:14 +01:00
|
|
|
if fn.endswith("/torch_c_api.cpp"):
|
|
|
|
|
# Begrudgingly tolerated because PyTorch has no C API.
|
|
|
|
|
run_remote_tool("clangformat", fn)
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(f"C++ is not supported.")
|
2022-01-22 17:50:07 +01:00
|
|
|
|
2021-03-11 13:07:22 +01:00
|
|
|
if re.match(r"(.*/PACKAGE)|(.*\.py)$", fn):
|
2020-08-18 18:27:16 +02:00
|
|
|
ast.parse(orig_content, filename=fn)
|
|
|
|
|
run_remote_tool("black", fn)
|
|
|
|
|
|
2021-03-11 13:07:22 +01:00
|
|
|
if re.match(r".*\.sh$", fn):
|
2021-02-17 15:58:35 +01:00
|
|
|
run_remote_tool("shfmt", fn)
|
2021-03-10 11:13:27 +01:00
|
|
|
run_remote_tool("shellcheck", fn)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
2021-03-11 13:07:22 +01:00
|
|
|
if re.match(r".*\.md$", fn):
|
2022-11-05 22:42:32 +01:00
|
|
|
run_remote_tool("mdformat", fn)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
2021-11-29 18:50:59 +01:00
|
|
|
if re.match(r"(.*/CMakeLists.txt)|(.*\.cmake)$", fn):
|
|
|
|
|
run_remote_tool("cmakeformat", fn)
|
|
|
|
|
|
2021-03-11 13:07:22 +01:00
|
|
|
if re.match(r"./data/.*POTENTIALS?$", fn):
|
2020-08-25 22:05:32 +02:00
|
|
|
check_data_files()
|
2020-08-25 12:11:03 +02:00
|
|
|
|
2022-02-06 14:13:52 +01:00
|
|
|
if re.match(r".*/Makefile", fn):
|
|
|
|
|
run_local_tool("./tools/precommit/format_makefile.py", fn)
|
|
|
|
|
|
2023-12-18 20:52:09 +01:00
|
|
|
if re.match(r".*\.inp$", fn):
|
|
|
|
|
run_local_tool("./tools/precommit/format_input_file.py", fn)
|
|
|
|
|
|
2021-07-23 22:16:57 +02:00
|
|
|
run_check_file_properties(fn)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
2020-08-21 00:30:35 +02:00
|
|
|
new_content = Path(fn).read_bytes()
|
2020-08-18 18:27:16 +02:00
|
|
|
if new_content == orig_content:
|
2020-08-21 00:30:35 +02:00
|
|
|
bak_fn.unlink() # remove backup
|
2020-08-18 18:27:16 +02:00
|
|
|
|
2020-08-21 00:30:35 +02:00
|
|
|
elif not allow_modifications:
|
|
|
|
|
bak_fn.replace(fn) # restore origin content
|
2022-07-16 13:47:33 +02:00
|
|
|
diff: Iterator[str]
|
2020-08-18 18:27:16 +02:00
|
|
|
try:
|
|
|
|
|
orig_lines = orig_content.decode("utf8").split("\n")
|
|
|
|
|
new_lines = new_content.decode("utf8").split("\n")
|
|
|
|
|
diff = unified_diff(orig_lines, new_lines, "before", "after", lineterm="")
|
2022-02-03 19:35:01 +09:00
|
|
|
except Exception:
|
2022-07-16 13:47:33 +02:00
|
|
|
diff = iter([])
|
2020-08-18 18:27:16 +02:00
|
|
|
raise Exception(f"File modified:\n" + "\n".join(diff))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def run_prettify(fn: str) -> None:
|
2020-08-18 18:27:16 +02:00
|
|
|
if fn in ("./src/base/base_uses.f90", "./src/common/util.F"):
|
|
|
|
|
return # Skipping because of prettify bugs.
|
|
|
|
|
|
|
|
|
|
# The prettify tool processes only about 1k lines of code per second.
|
|
|
|
|
# Hence, setting a generous timeout as our largest file has 100k lines.
|
2020-08-30 17:35:05 +02:00
|
|
|
run_local_tool("./tools/prettify/prettify.py", fn, timeout=600)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def run_check_file_properties(fn: str) -> None:
|
2021-03-11 13:07:22 +01:00
|
|
|
run_local_tool("./tools/precommit/check_file_properties.py", fn)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
|
2020-08-25 22:05:32 +02:00
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def check_data_files() -> None:
|
2020-08-25 22:05:32 +02:00
|
|
|
potential_files = [f"./data/{x}_POTENTIALS" for x in ("GTH", "HF", "NLCC", "ALL")]
|
|
|
|
|
expected_content = "".join([Path(fn).read_text() for fn in potential_files])
|
|
|
|
|
if Path("./data/POTENTIAL").read_text() != expected_content:
|
|
|
|
|
raise Exception("The data files are out of sync - please run `make data`.")
|
|
|
|
|
|
|
|
|
|
|
2020-08-18 18:27:16 +02:00
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def run_local_tool(*cmd: str, timeout: int = 20) -> None:
|
2020-08-18 18:27:16 +02:00
|
|
|
p = subprocess.run(cmd, timeout=timeout, stdout=PIPE, stderr=STDOUT)
|
|
|
|
|
if p.returncode != 0:
|
|
|
|
|
raise Exception(p.stdout.decode("utf8"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def run_remote_tool(tool: str, fn: str) -> None:
|
2020-08-18 18:27:16 +02:00
|
|
|
url = f"{SERVER}/{tool}"
|
|
|
|
|
r = http_post(url, fn)
|
|
|
|
|
if r.status == 304:
|
|
|
|
|
pass # file not modified
|
|
|
|
|
elif r.status == 200:
|
2020-08-21 00:30:35 +02:00
|
|
|
Path(fn).write_bytes(r.read())
|
2020-08-18 18:27:16 +02:00
|
|
|
else:
|
|
|
|
|
raise Exception(r.read().decode("utf8")) # something went wrong
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
2022-07-16 13:47:33 +02:00
|
|
|
def http_post(url: str, fn: str) -> HTTPResponse:
|
2020-08-18 18:27:16 +02:00
|
|
|
# This would be so much easier with the Requests library where it'd be a one-liner:
|
2020-08-21 00:30:35 +02:00
|
|
|
# return requests.post(url, files={Path(fn).name: Path(fn).read_bytes()})
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
boundary = uuid.uuid1().hex
|
2020-08-21 00:30:35 +02:00
|
|
|
name = Path(fn).name
|
2020-08-18 18:27:16 +02:00
|
|
|
data = b"".join(
|
|
|
|
|
[
|
|
|
|
|
f"--{boundary}\r\nContent-Disposition: ".encode("utf8"),
|
|
|
|
|
f'form-data; name="{name}"; filename="{name}"\r\n\r\n'.encode("utf8"),
|
2020-08-21 00:30:35 +02:00
|
|
|
Path(fn).read_bytes(),
|
2020-08-18 18:27:16 +02:00
|
|
|
f"\r\n--{boundary}--\r\n".encode("utf8"),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
headers = {
|
2022-07-16 13:47:33 +02:00
|
|
|
"Content-Length": f"{len(data)}",
|
2020-08-18 18:27:16 +02:00
|
|
|
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
|
|
|
}
|
|
|
|
|
try:
|
2022-07-16 13:47:33 +02:00
|
|
|
response = urlopen(Request(url, data=data, headers=headers), timeout=60)
|
|
|
|
|
return cast(HTTPResponse, response)
|
2020-08-18 18:27:16 +02:00
|
|
|
except HTTPError as err:
|
2022-07-16 13:47:33 +02:00
|
|
|
# HTTPError has .status and .read() just like a normal HTTPResponse.
|
|
|
|
|
return cast(HTTPResponse, err)
|
2020-08-18 18:27:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ======================================================================================
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
# EOF
|