blob: 8bfb3f5601875bfca3c59b27ac5d5e87151b1af4 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2023-2024 Arm Limited.
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import argparse
import datetime
import difflib
import filecmp
import logging
import os
import re
import subprocess
import sys
from modules.Shell import Shell
logger = logging.getLogger("format_code")
# List of directories to exclude
exceptions = [
"src/core/NEON/kernels/assembly/gemm",
"src/core/NEON/kernels/assembly/arm",
"/winograd/",
"/convolution/",
"/arm_gemm/",
"/arm_conv/",
"SConscript",
"SConstruct"
]
def adjust_copyright_year(copyright_years, curr_year):
ret_copyright_year = str()
# Read last year in the Copyright
last_year = int(copyright_years[-4:])
if last_year == curr_year:
ret_copyright_year = copyright_years
elif last_year == (curr_year - 1):
# Create range if latest year on the copyright is the previous
if len(copyright_years) > 4 and copyright_years[-5] == "-":
# Range already exists, update year to current
ret_copyright_year = copyright_years[:-5] + "-" + str(curr_year)
else:
# Create a new range
ret_copyright_year = copyright_years + "-" + str(curr_year)
else:
ret_copyright_year = copyright_years + ", " + str(curr_year)
return ret_copyright_year
def check_copyright( filename ):
f = open(filename, "r")
content = f.readlines()
f.close()
f = open(filename, "w")
year = datetime.datetime.now().year
ref = open("scripts/copyright_mit.txt","r").readlines()
# Need to handle python files separately
if("SConstruct" in filename or "SConscript" in filename):
start = 2
if("SConscript" in filename):
start = 3
m = re.match(r"(# Copyright \(c\) )(.*\d{4})( [Arm|ARM].*)", content[start])
line = m.group(1)
if m.group(2): # Is there a year already?
# Yes: adjust accordingly
line += adjust_copyright_year(m.group(2), year)
else:
# No: add current year
line += str(year)
line += m.group(3).replace("ARM", "Arm")
if("SConscript" in filename):
f.write('#!/usr/bin/python\n')
f.write('# -*- coding: utf-8 -*-\n\n')
f.write(line+"\n")
# Copy the rest of the file's content:
f.write("".join(content[start + 1:]))
f.close()
return
# This only works until year 9999
m = re.match(r"(.*Copyright \(c\) )(.*\d{4})( [Arm|ARM].*)", content[1])
start =len(ref)+2
if content[0] != "/*\n" or not m:
start = 0
f.write("/*\n * Copyright (c) %d Arm Limited.\n" % year)
else:
logger.debug("Found Copyright start")
logger.debug("\n\t".join([ g or "" for g in m.groups()]))
line = m.group(1)
if m.group(2): # Is there a year already?
# Yes: adjust accordingly
line += adjust_copyright_year(m.group(2), year)
else:
# No: add current year
line += str(year)
line += m.group(3).replace("ARM", "Arm")
f.write("/*\n"+line+"\n")
logger.debug(line)
# Write out the rest of the Copyright header:
for i in range(1, len(ref)):
line = ref[i]
f.write(" *")
if line.rstrip() != "":
f.write(" %s" % line)
else:
f.write("\n")
f.write(" */\n")
# Copy the rest of the file's content:
f.write("".join(content[start:]))
f.close()
def check_license(filename):
"""
Check that the license file is up-to-date
"""
f = open(filename, "r")
content = f.readlines()
f.close()
f = open(filename, "w")
f.write("".join(content[:2]))
year = datetime.datetime.now().year
# This only works until year 9999
m = re.match(r"(.*Copyright \(c\) )(.*\d{4})( [Arm|ARM].*)", content[2])
if not m:
f.write("Copyright (c) {} Arm Limited\n".format(year))
else:
updated_year = adjust_copyright_year(m.group(2), year)
f.write("Copyright (c) {} Arm Limited\n".format(updated_year))
# Copy the rest of the file's content:
f.write("".join(content[3:]))
f.close()
class OtherChecksRun:
def __init__(self, folder, error_diff=False, strategy="all"):
self.folder = folder
self.error_diff=error_diff
self.strategy = strategy
def error_on_diff(self, msg):
retval = 0
if self.error_diff:
diff = self.shell.run_single_to_str("git diff")
if len(diff) > 0:
retval = -1
logger.error(diff)
logger.error("\n"+msg)
return retval
def run(self):
retval = 0
self.shell = Shell()
self.shell.save_cwd()
this_dir = os.path.dirname(__file__)
self.shell.cd(self.folder)
self.shell.prepend_env("PATH","%s/../bin" % this_dir)
to_check = ""
if self.strategy != "all":
to_check, skip_copyright = FormatCodeRun.get_files(self.folder, self.strategy)
#FIXME: Exclude shaders!
logger.info("Running ./scripts/format_doxygen.py")
logger.debug(self.shell.run_single_to_str("./scripts/format_doxygen.py %s" % " ".join(to_check)))
retval = self.error_on_diff("Doxygen comments badly formatted (check above diff output for more details) try to run ./scripts/format_doxygen.py on your patch and resubmit")
if retval == 0:
logger.info("Running ./scripts/include_functions_kernels.py")
logger.debug(self.shell.run_single_to_str("python ./scripts/include_functions_kernels.py"))
retval = self.error_on_diff("Some kernels or functions are not included in their corresponding master header (check above diff output to see which includes are missing)")
if retval == 0:
try:
logger.info("Running ./scripts/check_bad_style.sh")
logger.debug(self.shell.run_single_to_str("./scripts/check_bad_style.sh"))
#logger.debug(self.shell.run_single_to_str("./scripts/check_bad_style.sh %s" % " ".join(to_check)))
except subprocess.CalledProcessError as e:
logger.error("Command %s returned:\n%s" % (e.cmd, e.output))
retval -= 1
if retval != 0:
raise Exception("format-code failed with error code %d" % retval)
class FormatCodeRun:
@staticmethod
def get_files(folder, strategy="git-head"):
shell = Shell()
shell.cd(folder)
skip_copyright = False
if strategy == "git-head":
cmd = "git diff-tree --no-commit-id --name-status -r HEAD | grep \"^[AMRT]\" | cut -f 2"
elif strategy == "git-diff":
cmd = "git diff --name-status --cached -r HEAD | grep \"^[AMRT]\" | rev | cut -f 1 | rev"
else:
cmd = "git ls-tree -r HEAD --name-only"
# Skip copyright checks when running on all files because we don't know when they were last modified
# Therefore we can't tell if their copyright dates are correct
skip_copyright = True
grep_folder = "grep -e \"^\\(arm_compute\\|src\\|examples\\|tests\\|utils\\|support\\)/\""
grep_extension = "grep -e \"\\.\\(cpp\\|h\\|hh\\|inl\\|cl\\|cs\\|hpp\\)$\""
list_files = shell.run_single_to_str(cmd+" | { "+ grep_folder+" | "+grep_extension + " || true; }")
to_check = [ f for f in list_files.split("\n") if len(f) > 0]
# Check for scons files as they are excluded from the above list
list_files = shell.run_single_to_str(cmd+" | { grep -e \"SC\" || true; }")
to_check += [ f for f in list_files.split("\n") if len(f) > 0]
return (to_check, skip_copyright)
def __init__(self, files, folder, error_diff=False, skip_copyright=False):
self.files = files
self.folder = folder
self.skip_copyright = skip_copyright
self.error_diff=error_diff
def error_on_diff(self, msg):
retval = 0
if self.error_diff:
diff = self.shell.run_single_to_str("git diff")
if len(diff) > 0:
retval = -1
logger.error(diff)
logger.error("\n"+msg)
return retval
def run(self):
if len(self.files) < 1:
logger.debug("No file: early exit")
retval = 0
self.shell = Shell()
self.shell.save_cwd()
this_dir = os.path.dirname(__file__)
try:
self.shell.cd(self.folder)
self.shell.prepend_env("PATH","%s/../bin" % this_dir)
for f in self.files:
if not self.skip_copyright:
check_copyright(f)
skip_this_file = False
for e in exceptions:
if e in f:
logger.warning("Skipping '%s' file: %s" % (e,f))
skip_this_file = True
break
if skip_this_file:
continue
logger.info("Formatting %s" % f)
check_license("LICENSE")
except subprocess.CalledProcessError as e:
retval = -1
logger.error(e)
logger.error("OUTPUT= %s" % e.output)
retval += self.error_on_diff("See above for clang-tidy errors")
if retval != 0:
raise Exception("format-code failed with error code %d" % retval)
class GenerateAndroidBP:
def __init__(self, folder):
self.folder = folder
self.bp_output_file = "Generated_Android.bp"
def run(self):
retval = 0
self.shell = Shell()
self.shell.save_cwd()
this_dir = os.path.dirname(__file__)
logger.debug("Running Android.bp check")
try:
self.shell.cd(self.folder)
cmd = "%s/generate_android_bp.py --folder %s --output_file %s" % (this_dir, self.folder, self.bp_output_file)
output = self.shell.run_single_to_str(cmd)
if len(output) > 0:
logger.info(output)
except subprocess.CalledProcessError as e:
retval = -1
logger.error(e)
logger.error("OUTPUT= %s" % e.output)
# Compare the genereated file with the one in the review
if not filecmp.cmp(self.bp_output_file, self.folder + "/Android.bp"):
is_mismatched = True
with open(self.bp_output_file, 'r') as generated_file:
with open(self.folder + "/Android.bp", 'r') as review_file:
diff = list(difflib.unified_diff(generated_file.readlines(), review_file.readlines(),
fromfile='Generated_Android.bp', tofile='Android.bp'))
# If the only mismatch in Android.bp file is the copyright year,
# the content of the file is considered unchanged and we don't need to update
# the copyright year. This will resolve the issue that emerges every new year.
num_added_lines = 0
num_removed_lines = 0
last_added_line = ""
last_removed_line = ""
expect_add_line = False
for line in diff:
if line.startswith("-") and not line.startswith("---"):
num_removed_lines += 1
if num_removed_lines > 1:
break
last_removed_line = line
expect_add_line = True
elif line.startswith("+") and not line.startswith("+++"):
num_added_lines += 1
if num_added_lines > 1:
break
if expect_add_line:
last_added_line = line
else:
expect_add_line = False
if num_added_lines == 1 and num_removed_lines == 1:
re_copyright = re.compile("^(?:\+|\-)// Copyright © ([0-9]+)\-([0-9]+) Arm Ltd. All rights reserved.\n$")
generated_matches = re_copyright.search(last_removed_line)
review_matches = re_copyright.search(last_added_line)
if generated_matches is not None and review_matches is not None:
if generated_matches.group(1) == review_matches.group(1) and \
int(generated_matches.group(2)) > int(review_matches.group(2)):
is_mismatched = False
if is_mismatched:
logger.error("Lines with '-' need to be added to Android.bp")
logger.error("Lines with '+' need to be removed from Android.bp")
for line in diff:
logger.error(line.rstrip())
if is_mismatched:
raise Exception("Android bp file is not updated")
if retval != 0:
raise Exception("generate Android bp file failed with error code %d" % retval)
def run_fix_code_formatting( files="git-head", folder=".", num_threads=1, error_on_diff=True):
try:
retval = 0
# Genereate Android.bp file and test it
gen_android_bp = GenerateAndroidBP(folder)
gen_android_bp.run()
to_check, skip_copyright = FormatCodeRun.get_files(folder, files)
other_checks = OtherChecksRun(folder,error_on_diff, files)
other_checks.run()
logger.debug(to_check)
num_files = len(to_check)
per_thread = max( num_files / num_threads,1)
start=0
logger.info("Files to format:\n\t%s" % "\n\t".join(to_check))
for i in range(num_threads):
if i == num_threads -1:
end = num_files
else:
end= min(start+per_thread, num_files)
sub = to_check[start:end]
logger.debug("[%d] [%d,%d] %s" % (i, start, end, sub))
start = end
format_code_run = FormatCodeRun(sub, folder, skip_copyright=skip_copyright)
format_code_run.run()
return retval
except Exception as e:
logger.error("Exception caught in run_fix_code_formatting: %s" % e)
return -1
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Build & run pre-commit tests",
)
file_sources=["git-diff","git-head","all"]
parser.add_argument("-D", "--debug", action='store_true', help="Enable script debugging output")
parser.add_argument("--error_on_diff", action='store_true', help="Show diff on error and stop")
parser.add_argument("--files", nargs='?', metavar="source", choices=file_sources, help="Which files to run fix_code_formatting on, choices=%s" % file_sources, default="git-head")
parser.add_argument("--folder", metavar="path", help="Folder in which to run fix_code_formatting", default=".")
args = parser.parse_args()
logging_level = logging.INFO
if args.debug:
logging_level = logging.DEBUG
logging.basicConfig(level=logging_level)
logger.debug("Arguments passed: %s" % str(args.__dict__))
exit(run_fix_code_formatting(args.files, args.folder, 1, error_on_diff=args.error_on_diff))