blob: fa572cf51c5caed75271755982d6c1f197d5b6da [file] [log] [blame]
Gunes Bayir66b4a6a2023-07-01 22:55:42 +01001#!/usr/bin/env python3
2
3# Copyright (c) 2023 Arm Limited.
4#
5# SPDX-License-Identifier: MIT
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to
9# deal in the Software without restriction, including without limitation the
10# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11# sell copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in all
15# copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23# SOFTWARE.
24
25import argparse
26import datetime
27import difflib
28import filecmp
29import logging
30import os
31import re
32import subprocess
33import sys
34
35from modules.Shell import Shell
36
37logger = logging.getLogger("format_code")
38
39ASTYLE_PARAMETERS ="--style=ansi \
40 --indent=spaces \
41 --indent-switches \
42 --indent-col1-comments \
43 --min-conditional-indent=0 \
44 --max-instatement-indent=120 \
45 --pad-oper \
46 --align-pointer=name \
47 --align-reference=name \
48 --break-closing-brackets \
49 --keep-one-line-statements \
50 --max-code-length=200 \
51 --mode=c \
52 --lineend=linux \
53 --indent-preprocessor \
54 "
55
56exceptions = [
57 "src/core/NEON/kernels/assembly/gemm",
58 "src/core/NEON/kernels/assembly/arm",
59 "/winograd/",
60 "/convolution/",
61 "/arm_gemm/",
62 "/arm_conv/",
63 "compute_kernel_writer/"
64]
65
66def adjust_copyright_year(copyright_years, curr_year):
67 ret_copyright_year = str()
68 # Read last year in the Copyright
69 last_year = int(copyright_years[-4:])
70 if last_year == curr_year:
71 ret_copyright_year = copyright_years
72 elif last_year == (curr_year - 1):
73 # Create range if latest year on the copyright is the previous
74 if len(copyright_years) > 4 and copyright_years[-5] == "-":
75 # Range already exists, update year to current
76 ret_copyright_year = copyright_years[:-5] + "-" + str(curr_year)
77 else:
78 # Create a new range
79 ret_copyright_year = copyright_years + "-" + str(curr_year)
80 else:
81 ret_copyright_year = copyright_years + ", " + str(curr_year)
82 return ret_copyright_year
83
84def check_copyright( filename ):
85 f = open(filename, "r")
86 content = f.readlines()
87 f.close()
88 f = open(filename, "w")
89 year = datetime.datetime.now().year
90 ref = open("scripts/copyright_mit.txt","r").readlines()
91
92 # Need to handle python files separately
93 if("SConstruct" in filename or "SConscript" in filename):
94 start = 2
95 if("SConscript" in filename):
96 start = 3
97 m = re.match("(# Copyright \(c\) )(.*\d{4})( [Arm|ARM].*)", content[start])
98 line = m.group(1)
99
100 if m.group(2): # Is there a year already?
101 # Yes: adjust accordingly
102 line += adjust_copyright_year(m.group(2), year)
103 else:
104 # No: add current year
105 line += str(year)
106 line += m.group(3).replace("ARM", "Arm")
107 if("SConscript" in filename):
108 f.write('#!/usr/bin/python\n')
109
110 f.write('# -*- coding: utf-8 -*-\n\n')
111 f.write(line+"\n")
112 # Copy the rest of the file's content:
113 f.write("".join(content[start + 1:]))
114 f.close()
115
116 return
117
118 # This only works until year 9999
119 m = re.match("(.*Copyright \(c\) )(.*\d{4})( [Arm|ARM].*)", content[1])
120 start =len(ref)+2
121 if content[0] != "/*\n" or not m:
122 start = 0
123 f.write("/*\n * Copyright (c) %d Arm Limited.\n" % year)
124 else:
125 logger.debug("Found Copyright start")
126 logger.debug("\n\t".join([ g or "" for g in m.groups()]))
127 line = m.group(1)
128
129 if m.group(2): # Is there a year already?
130 # Yes: adjust accordingly
131 line += adjust_copyright_year(m.group(2), year)
132 else:
133 # No: add current year
134 line += str(year)
135 line += m.group(3).replace("ARM", "Arm")
136 f.write("/*\n"+line+"\n")
137 logger.debug(line)
138 # Write out the rest of the Copyright header:
139 for i in range(1, len(ref)):
140 line = ref[i]
141 f.write(" *")
142 if line.rstrip() != "":
143 f.write(" %s" % line)
144 else:
145 f.write("\n")
146 f.write(" */\n")
147 # Copy the rest of the file's content:
148 f.write("".join(content[start:]))
149 f.close()
150
151def check_license(filename):
152 """
153 Check that the license file is up-to-date
154 """
155 f = open(filename, "r")
156 content = f.readlines()
157 f.close()
158
159 f = open(filename, "w")
160 f.write("".join(content[:2]))
161
162 year = datetime.datetime.now().year
163 # This only works until year 9999
164 m = re.match("(.*Copyright \(c\) )(.*\d{4})( [Arm|ARM].*)", content[2])
165
166 if not m:
167 f.write("Copyright (c) {} Arm Limited\n".format(year))
168 else:
169 updated_year = adjust_copyright_year(m.group(2), year)
170 f.write("Copyright (c) {} Arm Limited\n".format(updated_year))
171
172 # Copy the rest of the file's content:
173 f.write("".join(content[3:]))
174 f.close()
175
176
177class OtherChecksRun:
178 def __init__(self, folder, error_diff=False, strategy="all"):
179 self.folder = folder
180 self.error_diff=error_diff
181 self.strategy = strategy
182
183 def error_on_diff(self, msg):
184 retval = 0
185 if self.error_diff:
186 diff = self.shell.run_single_to_str("git diff")
187 if len(diff) > 0:
188 retval = -1
189 logger.error(diff)
190 logger.error("\n"+msg)
191 return retval
192
193 def run(self):
194 retval = 0
195 self.shell = Shell()
196 self.shell.save_cwd()
197 this_dir = os.path.dirname(__file__)
198 self.shell.cd(self.folder)
199 self.shell.prepend_env("PATH","%s/../bin" % this_dir)
200
201 to_check = ""
202 if self.strategy != "all":
203 to_check, skip_copyright = FormatCodeRun.get_files(self.folder, self.strategy)
204 #FIXME: Exclude shaders!
205
206 logger.info("Running ./scripts/format_doxygen.py")
207 logger.debug(self.shell.run_single_to_str("./scripts/format_doxygen.py %s" % " ".join(to_check)))
208 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")
209 if retval == 0:
210 logger.info("Running ./scripts/include_functions_kernels.py")
211 logger.debug(self.shell.run_single_to_str("python ./scripts/include_functions_kernels.py"))
212 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)")
213 if retval == 0:
214 try:
215 logger.info("Running ./scripts/check_bad_style.sh")
216 logger.debug(self.shell.run_single_to_str("./scripts/check_bad_style.sh"))
217 #logger.debug(self.shell.run_single_to_str("./scripts/check_bad_style.sh %s" % " ".join(to_check)))
218 except subprocess.CalledProcessError as e:
219 logger.error("Command %s returned:\n%s" % (e.cmd, e.output))
220 retval -= 1
221
222 if retval != 0:
223 raise Exception("format-code failed with error code %d" % retval)
224
225class FormatCodeRun:
226 @staticmethod
227 def get_files(folder, strategy="git-head"):
228 shell = Shell()
229 shell.cd(folder)
230 skip_copyright = False
231 if strategy == "git-head":
232 cmd = "git diff-tree --no-commit-id --name-status -r HEAD | grep \"^[AMRT]\" | cut -f 2"
233 elif strategy == "git-diff":
234 cmd = "git diff --name-status --cached -r HEAD | grep \"^[AMRT]\" | cut -f 2"
235 else:
236 cmd = "git ls-tree -r HEAD --name-only"
237 # Skip copyright checks when running on all files because we don't know when they were last modified
238 # Therefore we can't tell if their copyright dates are correct
239 skip_copyright = True
240
241 grep_folder = "grep -e \"^\\(arm_compute\\|src\\|examples\\|tests\\|utils\\|support\\)/\""
242 grep_extension = "grep -e \"\\.\\(cpp\\|h\\|inl\\|cl\\|cs\\|hpp\\)$\""
243 list_files = shell.run_single_to_str(cmd+" | { "+ grep_folder+" | "+grep_extension + " || true; }")
244 to_check = [ f for f in list_files.split("\n") if len(f) > 0]
245
246 # Check for scons files as they are excluded from the above list
247 list_files = shell.run_single_to_str(cmd+" | { grep -e \"SC\" || true; }")
248 to_check += [ f for f in list_files.split("\n") if len(f) > 0]
249
250 return (to_check, skip_copyright)
251
252 def __init__(self, files, folder, error_diff=False, skip_copyright=False):
253 self.files = files
254 self.folder = folder
255 self.skip_copyright = skip_copyright
256 self.error_diff=error_diff
257
258 def error_on_diff(self, msg):
259 retval = 0
260 if self.error_diff:
261 diff = self.shell.run_single_to_str("git diff")
262 if len(diff) > 0:
263 retval = -1
264 logger.error(diff)
265 logger.error("\n"+msg)
266 return retval
267
268 def run(self):
269 if len(self.files) < 1:
270 logger.debug("No file: early exit")
271 retval = 0
272 self.shell = Shell()
273 self.shell.save_cwd()
274 this_dir = os.path.dirname(__file__)
275 try:
276 self.shell.cd(self.folder)
277 self.shell.prepend_env("PATH","%s/../bin" % this_dir)
278 clang_format = "clang-format -i -style=file "
279 astyle = "astyle -n -q %s " % (ASTYLE_PARAMETERS)
280
281 if sys.platform == 'darwin':
282 # this platform explicitly needs an extension for the temporary file
283 sed = "sed -i '.log' 's/\\t/ /g' "
284 else:
285 sed = "sed -i 's/\\t/ /g' "
286
287 single_eol = "%s/ensure_single_eol.py " % this_dir
288 for f in self.files:
289 skip_this_file = False
290 for e in exceptions:
291 if e in f:
292 logger.warning("Skipping '%s' file: %s" % (e,f))
293 skip_this_file = True
294 break
295 if skip_this_file:
296 continue
297
298 logger.info("Formatting %s" % f)
299 if not self.skip_copyright:
300 check_copyright(f)
301 cmds = [
302 sed + f,
303 clang_format + f,
304 astyle + f,
305 single_eol + f
306 ]
307
308 if sys.platform == 'darwin':
309 # the temporary file creted by 'sed' will be removed here
310 cmds.append(f"rm {f}.log")
311
312 for cmd in cmds:
313 output = self.shell.run_single_to_str(cmd)
314 if len(output) > 0:
315 logger.info(output)
316
317 check_license("LICENSE")
318
319 except subprocess.CalledProcessError as e:
320 retval = -1
321 logger.error(e)
322 logger.error("OUTPUT= %s" % e.output)
323
324 retval += self.error_on_diff("See above for clang-tidy errors")
325
326 if retval != 0:
327 raise Exception("format-code failed with error code %d" % retval)
328
329class GenerateAndroidBP:
330 def __init__(self, folder):
331 self.folder = folder
332 self.bp_output_file = "Generated_Android.bp"
333
334 def run(self):
335 retval = 0
336 self.shell = Shell()
337 self.shell.save_cwd()
338 this_dir = os.path.dirname(__file__)
339
340 logger.debug("Running Android.bp check")
341 try:
342 self.shell.cd(self.folder)
343 cmd = "%s/generate_android_bp.py --folder %s --output_file %s" % (this_dir, self.folder, self.bp_output_file)
344 output = self.shell.run_single_to_str(cmd)
345 if len(output) > 0:
346 logger.info(output)
347 except subprocess.CalledProcessError as e:
348 retval = -1
349 logger.error(e)
350 logger.error("OUTPUT= %s" % e.output)
351
352 # Compare the genereated file with the one in the review
353 if not filecmp.cmp(self.bp_output_file, self.folder + "/Android.bp"):
354 is_mismatched = True
355
356 with open(self.bp_output_file, 'r') as generated_file:
357 with open(self.folder + "/Android.bp", 'r') as review_file:
358 diff = list(difflib.unified_diff(generated_file.readlines(), review_file.readlines(),
359 fromfile='Generated_Android.bp', tofile='Android.bp'))
360
361 # If the only mismatch in Android.bp file is the copyright year,
362 # the content of the file is considered unchanged and we don't need to update
363 # the copyright year. This will resolve the issue that emerges every new year.
364 num_added_lines = 0
365 num_removed_lines = 0
366 last_added_line = ""
367 last_removed_line = ""
368 expect_add_line = False
369
370 for line in diff:
371 if line.startswith("-") and not line.startswith("---"):
372 num_removed_lines += 1
373 if num_removed_lines > 1:
374 break
375 last_removed_line = line
376 expect_add_line = True
377 elif line.startswith("+") and not line.startswith("+++"):
378 num_added_lines += 1
379 if num_added_lines > 1:
380 break
381 if expect_add_line:
382 last_added_line = line
383 else:
384 expect_add_line = False
385
386 if num_added_lines == 1 and num_removed_lines == 1:
387 re_copyright = re.compile("^(?:\+|\-)// Copyright © ([0-9]+)\-([0-9]+) Arm Ltd. All rights reserved.\n$")
388 generated_matches = re_copyright.search(last_removed_line)
389 review_matches = re_copyright.search(last_added_line)
390
391 if generated_matches is not None and review_matches is not None:
392 if generated_matches.group(1) == review_matches.group(1) and \
393 int(generated_matches.group(2)) > int(review_matches.group(2)):
394 is_mismatched = False
395
396 if is_mismatched:
397 logger.error("Lines with '-' need to be added to Android.bp")
398 logger.error("Lines with '+' need to be removed from Android.bp")
399
400 for line in diff:
401 logger.error(line.rstrip())
402 if is_mismatched:
403 raise Exception("Android bp file is not updated")
404
405 if retval != 0:
406 raise Exception("generate Android bp file failed with error code %d" % retval)
407
408def run_fix_code_formatting( files="git-head", folder=".", num_threads=1, error_on_diff=True):
409 try:
410 retval = 0
411
412 # Genereate Android.bp file and test it
413 gen_android_bp = GenerateAndroidBP(folder)
414 gen_android_bp.run()
415
416 to_check, skip_copyright = FormatCodeRun.get_files(folder, files)
417 other_checks = OtherChecksRun(folder,error_on_diff, files)
418 other_checks.run()
419
420 logger.debug(to_check)
421 num_files = len(to_check)
422 per_thread = max( num_files / num_threads,1)
423 start=0
424 logger.info("Files to format:\n\t%s" % "\n\t".join(to_check))
425
426 for i in range(num_threads):
427 if i == num_threads -1:
428 end = num_files
429 else:
430 end= min(start+per_thread, num_files)
431 sub = to_check[start:end]
432 logger.debug("[%d] [%d,%d] %s" % (i, start, end, sub))
433 start = end
434 format_code_run = FormatCodeRun(sub, folder, skip_copyright=skip_copyright)
435 format_code_run.run()
436
437 return retval
438 except Exception as e:
439 logger.error("Exception caught in run_fix_code_formatting: %s" % e)
440 return -1
441
442if __name__ == "__main__":
443 parser = argparse.ArgumentParser(
444 formatter_class=argparse.RawDescriptionHelpFormatter,
445 description="Build & run pre-commit tests",
446 )
447
448 file_sources=["git-diff","git-head","all"]
449 parser.add_argument("-D", "--debug", action='store_true', help="Enable script debugging output")
450 parser.add_argument("--error_on_diff", action='store_true', help="Show diff on error and stop")
451 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")
452 parser.add_argument("--folder", metavar="path", help="Folder in which to run fix_code_formatting", default=".")
453
454 args = parser.parse_args()
455
456 logging_level = logging.INFO
457 if args.debug:
458 logging_level = logging.DEBUG
459
460 logging.basicConfig(level=logging_level)
461
462 logger.debug("Arguments passed: %s" % str(args.__dict__))
463
464 exit(run_fix_code_formatting(args.files, args.folder, 1, error_on_diff=args.error_on_diff))