blob: e64ed66cc49410d15c4986238944f8b8b6ca73ee [file] [log] [blame]
Shahbaz Youssefi98a35502019-01-08 22:09:39 -05001# Copyright 2019 The ANGLE Project Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Shahbaz Youssefi98a35502019-01-08 22:09:39 -05004"""Top-level presubmit script for code generation.
5
6See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
7for more details on the presubmit API built into depot_tools.
8"""
9
Brian Sheedycc4d8332019-09-11 17:26:00 -070010import os
Shahbaz Youssefi634ee432019-12-16 11:39:10 -050011import re
Brian Sheedycc4d8332019-09-11 17:26:00 -070012import shutil
13import subprocess
14import sys
15import tempfile
Shahbaz Youssefi98a35502019-01-08 22:09:39 -050016
James Darpinianb6ea6ed2020-12-29 16:30:29 -080017# Fragment of a regular expression that matches C/C++ and Objective-C++ implementation files and headers.
18_IMPLEMENTATION_AND_HEADER_EXTENSIONS = r'\.(c|cc|cpp|cxx|mm|h|hpp|hxx)$'
Jamie Madill73397e82019-01-09 10:33:16 -050019
Jamie Madill73397e82019-01-09 10:33:16 -050020# Fragment of a regular expression that matches C++ and Objective-C++ header files.
21_HEADER_EXTENSIONS = r'\.(h|hpp|hxx)$'
22
Brian Sheedycc4d8332019-09-11 17:26:00 -070023_PRIMARY_EXPORT_TARGETS = [
24 '//:libEGL',
25 '//:libGLESv1_CM',
26 '//:libGLESv2',
27 '//:translator',
28]
29
Jamie Madill73397e82019-01-09 10:33:16 -050030
Manh Nguyenebc6d0a2020-05-27 18:21:30 -040031def _CheckCommitMessageFormatting(input_api, output_api):
32
33 def _IsLineBlank(line):
34 return line.isspace() or line == ""
35
36 def _PopBlankLines(lines, reverse=False):
37 if reverse:
38 while len(lines) > 0 and _IsLineBlank(lines[-1]):
39 lines.pop()
40 else:
41 while len(lines) > 0 and _IsLineBlank(lines[0]):
42 lines.pop(0)
43
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040044 def _IsTagLine(line):
45 return ":" in line
46
Manh Nguyencb8eea22020-07-01 23:17:43 -040047 def _SplitIntoMultipleCommits(description_text):
48 paragraph_split_pattern = r"((?m)^\s*$\n)"
49 multiple_paragraphs = re.split(paragraph_split_pattern, description_text)
50 multiple_commits = [""]
51 change_id_pattern = re.compile(r"(?m)^Change-Id: [a-zA-Z0-9]*$")
52 for paragraph in multiple_paragraphs:
53 multiple_commits[-1] += paragraph
54 if change_id_pattern.search(paragraph):
55 multiple_commits.append("")
56 if multiple_commits[-1] == "":
57 multiple_commits.pop()
58 return multiple_commits
59
60 def _CheckTabInCommit(lines):
61 return all([line.find("\t") == -1 for line in lines])
62
Trevor David Blacke815afb2020-09-07 22:09:22 +000063 allowlist_strings = ['Revert "', 'Roll ', 'Reland ', 'Re-land ']
Manh Nguyenebc6d0a2020-05-27 18:21:30 -040064 summary_linelength_warning_lower_limit = 65
65 summary_linelength_warning_upper_limit = 70
Shahbaz Youssefi462e40f2020-06-04 14:15:59 -040066 description_linelength_limit = 72
Manh Nguyenebc6d0a2020-05-27 18:21:30 -040067
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040068 git_output = input_api.change.DescriptionText()
69
Manh Nguyencb8eea22020-07-01 23:17:43 -040070 multiple_commits = _SplitIntoMultipleCommits(git_output)
Manh Nguyenebc6d0a2020-05-27 18:21:30 -040071 errors = []
Manh Nguyenebc6d0a2020-05-27 18:21:30 -040072
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040073 for k in range(len(multiple_commits)):
Manh Nguyencb8eea22020-07-01 23:17:43 -040074 commit_msg_lines = multiple_commits[k].splitlines()
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040075 commit_number = len(multiple_commits) - k
76 commit_tag = "Commit " + str(commit_number) + ":"
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040077 commit_msg_line_numbers = {}
78 for i in range(len(commit_msg_lines)):
79 commit_msg_line_numbers[commit_msg_lines[i]] = i + 1
Manh Nguyenebc6d0a2020-05-27 18:21:30 -040080 _PopBlankLines(commit_msg_lines, True)
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040081 _PopBlankLines(commit_msg_lines, False)
Trevor David Blacke815afb2020-09-07 22:09:22 +000082 allowlisted = False
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040083 if len(commit_msg_lines) > 0:
Trevor David Blacke815afb2020-09-07 22:09:22 +000084 for allowlist_string in allowlist_strings:
85 if commit_msg_lines[0].startswith(allowlist_string):
86 allowlisted = True
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040087 break
Trevor David Blacke815afb2020-09-07 22:09:22 +000088 if allowlisted:
Manh Nguyen505b6eb2020-06-04 09:11:21 -040089 continue
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040090
Manh Nguyencb8eea22020-07-01 23:17:43 -040091 if not _CheckTabInCommit(commit_msg_lines):
Manh Nguyen31bbe1b2020-06-11 10:46:55 -040092 errors.append(
93 output_api.PresubmitError(commit_tag + "Tabs are not allowed in commit message."))
94
95 # the tags paragraph is at the end of the message
96 # the break between the tags paragraph is the first line without ":"
97 # this is sufficient because if a line is blank, it will not have ":"
98 last_paragraph_line_count = 0
99 while len(commit_msg_lines) > 0 and _IsTagLine(commit_msg_lines[-1]):
100 last_paragraph_line_count += 1
101 commit_msg_lines.pop()
102 if last_paragraph_line_count == 0:
Manh Nguyenebc6d0a2020-05-27 18:21:30 -0400103 errors.append(
104 output_api.PresubmitError(
Manh Nguyen31bbe1b2020-06-11 10:46:55 -0400105 commit_tag +
106 "Please ensure that there are tags (e.g., Bug:, Test:) in your description."))
107 if len(commit_msg_lines) > 0:
108 if not _IsLineBlank(commit_msg_lines[-1]):
109 output_api.PresubmitError(commit_tag +
110 "Please ensure that there exists 1 blank line " +
111 "between tags and description body.")
112 else:
113 # pop the blank line between tag paragraph and description body
114 commit_msg_lines.pop()
115 if len(commit_msg_lines) > 0 and _IsLineBlank(commit_msg_lines[-1]):
116 errors.append(
117 output_api.PresubmitError(
118 commit_tag + 'Please ensure that there exists only 1 blank line '
119 'between tags and description body.'))
120 # pop all the remaining blank lines between tag and description body
121 _PopBlankLines(commit_msg_lines, True)
122 if len(commit_msg_lines) == 0:
123 errors.append(
124 output_api.PresubmitError(commit_tag +
125 'Please ensure that your description summary'
126 ' and description body are not blank.'))
127 continue
128
129 if summary_linelength_warning_lower_limit <= len(commit_msg_lines[0]) \
130 <= summary_linelength_warning_upper_limit:
131 errors.append(
132 output_api.PresubmitPromptWarning(
133 commit_tag + "Your description summary should be on one line of " +
134 str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
135 elif len(commit_msg_lines[0]) > summary_linelength_warning_upper_limit:
136 errors.append(
137 output_api.PresubmitError(
138 commit_tag + "Please ensure that your description summary is on one line of " +
139 str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
140 commit_msg_lines.pop(0) # get rid of description summary
141 if len(commit_msg_lines) == 0:
142 continue
143 if not _IsLineBlank(commit_msg_lines[0]):
144 errors.append(
145 output_api.PresubmitError(commit_tag +
146 'Please ensure the summary is only 1 line and '
147 'there is 1 blank line between the summary '
148 'and description body.'))
149 else:
150 commit_msg_lines.pop(0) # pop first blank line
151 if len(commit_msg_lines) == 0:
152 continue
153 if _IsLineBlank(commit_msg_lines[0]):
154 errors.append(
155 output_api.PresubmitError(commit_tag +
156 'Please ensure that there exists only 1 blank line '
157 'between description summary and description body.'))
158 # pop all the remaining blank lines between
159 # description summary and description body
160 _PopBlankLines(commit_msg_lines)
161
162 # loop through description body
163 while len(commit_msg_lines) > 0:
164 line = commit_msg_lines.pop(0)
165 # lines starting with 4 spaces or lines without space(urls)
166 # are exempt from length check
167 if line.startswith(" ") or " " not in line:
168 continue
169 if len(line) > description_linelength_limit:
170 errors.append(
171 output_api.PresubmitError(
172 commit_tag + 'Line ' + str(commit_msg_line_numbers[line]) +
173 ' is too long.\n' + '"' + line + '"\n' + 'Please wrap it to ' +
174 str(description_linelength_limit) + ' characters. ' +
175 "Lines without spaces or lines starting with 4 spaces are exempt."))
176 break
Manh Nguyenebc6d0a2020-05-27 18:21:30 -0400177 return errors
178
179
Jamie Madill9e438ee2019-07-05 08:44:23 -0400180def _CheckChangeHasBugField(input_api, output_api):
Shahbaz Youssefi634ee432019-12-16 11:39:10 -0500181 """Requires that the changelist have a Bug: field from a known project."""
Jamie Madill9e438ee2019-07-05 08:44:23 -0400182 bugs = input_api.change.BugsFromDescription()
183 if not bugs:
184 return [
Alexis Hetu4210e492019-10-10 14:22:03 -0400185 output_api.PresubmitError('Please ensure that your description contains:\n'
186 '"Bug: angleproject:[bug number]"\n'
187 'directly above the Change-Id tag.')
Jamie Madill9e438ee2019-07-05 08:44:23 -0400188 ]
Shahbaz Youssefi634ee432019-12-16 11:39:10 -0500189
190 # The bug must be in the form of "project:number". None is also accepted, which is used by
191 # rollers as well as in very minor changes.
192 if len(bugs) == 1 and bugs[0] == 'None':
Jamie Madill9e438ee2019-07-05 08:44:23 -0400193 return []
194
Corentin Wallez2753f202021-04-06 14:04:02 +0200195 projects = [
196 'angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'tint:', 'b/'
197 ]
Geoff Lang13e4cdb2020-04-16 14:35:54 -0400198 bug_regex = re.compile(r"([a-z]+[:/])(\d+)")
Shahbaz Youssefi634ee432019-12-16 11:39:10 -0500199 errors = []
200 extra_help = None
201
202 for bug in bugs:
203 if bug == 'None':
204 errors.append(
205 output_api.PresubmitError('Invalid bug tag "None" in presence of other bug tags.'))
206 continue
207
208 match = re.match(bug_regex, bug)
209 if match == None or bug != match.group(0) or match.group(1) not in projects:
210 errors.append(output_api.PresubmitError('Incorrect bug tag "' + bug + '".'))
211 if not extra_help:
212 extra_help = output_api.PresubmitError('Acceptable format is:\n\n'
213 ' Bug: project:bugnumber\n\n'
214 'Acceptable projects are:\n\n ' +
215 '\n '.join(projects))
216
217 if extra_help:
218 errors.append(extra_help)
219
220 return errors
221
Jamie Madill9e438ee2019-07-05 08:44:23 -0400222
Jamie Madill73397e82019-01-09 10:33:16 -0500223def _CheckCodeGeneration(input_api, output_api):
Jamie Madill04e9e552019-04-01 14:40:21 -0400224
225 class Msg(output_api.PresubmitError):
Geoff Langd7d42392019-05-06 13:15:35 -0400226 """Specialized error message"""
227
228 def __init__(self, message):
229 super(output_api.PresubmitError, self).__init__(
230 message,
Jamie Madill8dfc2b02019-11-25 15:12:58 -0500231 long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n'
Jamie Madill9e438ee2019-07-05 08:44:23 -0400232 'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n'
233 '\n'
Jamie Madill8dfc2b02019-11-25 15:12:58 -0500234 'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n'
235 '\n'
Jamie Madill9e438ee2019-07-05 08:44:23 -0400236 'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n'
237 'before gclient sync. See the DevSetup documentation for more details.\n')
Jamie Madill04e9e552019-04-01 14:40:21 -0400238
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500239 code_gen_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
240 'scripts/run_code_generation.py')
241 cmd_name = 'run_code_generation'
242 cmd = [input_api.python_executable, code_gen_path, '--verify-no-dirty']
Geoff Langd7d42392019-05-06 13:15:35 -0400243 test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg)
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500244 if input_api.verbose:
245 print('Running ' + cmd_name)
246 return input_api.RunTests([test_cmd])
247
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500248
Jamie Madill73397e82019-01-09 10:33:16 -0500249# Taken directly from Chromium's PRESUBMIT.py
250def _CheckNewHeaderWithoutGnChange(input_api, output_api):
Geoff Langd7d42392019-05-06 13:15:35 -0400251 """Checks that newly added header files have corresponding GN changes.
Jamie Madill73397e82019-01-09 10:33:16 -0500252 Note that this is only a heuristic. To be precise, run script:
253 build/check_gn_headers.py.
254 """
255
Geoff Langd7d42392019-05-06 13:15:35 -0400256 def headers(f):
Trevor David Blackf69aaf62020-10-05 21:50:10 +0000257 return input_api.FilterSourceFile(f, files_to_check=(r'.+%s' % _HEADER_EXTENSIONS,))
Jamie Madill73397e82019-01-09 10:33:16 -0500258
Geoff Langd7d42392019-05-06 13:15:35 -0400259 new_headers = []
260 for f in input_api.AffectedSourceFiles(headers):
261 if f.Action() != 'A':
262 continue
263 new_headers.append(f.LocalPath())
Jamie Madill73397e82019-01-09 10:33:16 -0500264
Geoff Langd7d42392019-05-06 13:15:35 -0400265 def gn_files(f):
Trevor David Blackf69aaf62020-10-05 21:50:10 +0000266 return input_api.FilterSourceFile(f, files_to_check=(r'.+\.gn',))
Jamie Madill73397e82019-01-09 10:33:16 -0500267
Geoff Langd7d42392019-05-06 13:15:35 -0400268 all_gn_changed_contents = ''
269 for f in input_api.AffectedSourceFiles(gn_files):
270 for _, line in f.ChangedContents():
271 all_gn_changed_contents += line
Jamie Madill73397e82019-01-09 10:33:16 -0500272
Geoff Langd7d42392019-05-06 13:15:35 -0400273 problems = []
274 for header in new_headers:
275 basename = input_api.os_path.basename(header)
276 if basename not in all_gn_changed_contents:
277 problems.append(header)
Jamie Madill73397e82019-01-09 10:33:16 -0500278
Geoff Langd7d42392019-05-06 13:15:35 -0400279 if problems:
280 return [
281 output_api.PresubmitPromptWarning(
282 'Missing GN changes for new header files',
283 items=sorted(problems),
284 long_text='Please double check whether newly added header files need '
285 'corresponding changes in gn or gni files.\nThis checking is only a '
286 'heuristic. Run build/check_gn_headers.py to be precise.\n'
287 'Read https://crbug.com/661774 for more info.')
288 ]
289 return []
Jamie Madill73397e82019-01-09 10:33:16 -0500290
291
Brian Sheedycc4d8332019-09-11 17:26:00 -0700292def _CheckExportValidity(input_api, output_api):
293 outdir = tempfile.mkdtemp()
294 # shell=True is necessary on Windows, as otherwise subprocess fails to find
295 # either 'gn' or 'vpython3' even if they are findable via PATH.
296 use_shell = input_api.is_windows
297 try:
298 try:
299 subprocess.check_output(['gn', 'gen', outdir], shell=use_shell)
300 except subprocess.CalledProcessError as e:
301 return [
302 output_api.PresubmitError(
303 'Unable to run gn gen for export_targets.py: %s' % e.output)
304 ]
305 export_target_script = os.path.join(input_api.PresubmitLocalPath(), 'scripts',
306 'export_targets.py')
307 try:
308 subprocess.check_output(
309 ['vpython3', export_target_script, outdir] + _PRIMARY_EXPORT_TARGETS,
310 stderr=subprocess.STDOUT,
311 shell=use_shell)
312 except subprocess.CalledProcessError as e:
313 if input_api.is_committing:
314 return [output_api.PresubmitError('export_targets.py failed: %s' % e.output)]
315 return [
316 output_api.PresubmitPromptWarning(
317 'export_targets.py failed, this may just be due to your local checkout: %s' %
318 e.output)
319 ]
320 return []
321 finally:
322 shutil.rmtree(outdir)
323
324
James Darpinian4dff9952019-12-10 17:05:37 -0800325def _CheckTabsInSourceFiles(input_api, output_api):
326 """Forbids tab characters in source files due to a WebKit repo requirement. """
327
James Darpinianb6ea6ed2020-12-29 16:30:29 -0800328 def implementation_and_headers_including_third_party(f):
329 # Check third_party files too, because WebKit's checks don't make exceptions.
James Darpinian4dff9952019-12-10 17:05:37 -0800330 return input_api.FilterSourceFile(
James Darpinianb6ea6ed2020-12-29 16:30:29 -0800331 f,
332 files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,),
333 files_to_skip=[f for f in input_api.DEFAULT_FILES_TO_SKIP if not "third_party" in f])
James Darpinian4dff9952019-12-10 17:05:37 -0800334
335 files_with_tabs = []
James Darpinianb6ea6ed2020-12-29 16:30:29 -0800336 for f in input_api.AffectedSourceFiles(implementation_and_headers_including_third_party):
James Darpinian4dff9952019-12-10 17:05:37 -0800337 for (num, line) in f.ChangedContents():
338 if '\t' in line:
339 files_with_tabs.append(f)
340 break
341
342 if files_with_tabs:
343 return [
344 output_api.PresubmitError(
345 'Tab characters in source files.',
346 items=sorted(files_with_tabs),
347 long_text=
348 'Tab characters are forbidden in ANGLE source files because WebKit\'s Subversion\n'
349 'repository does not allow tab characters in source files.\n'
350 'Please remove tab characters from these files.')
351 ]
352 return []
353
354
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600355# https://stackoverflow.com/a/196392
356def is_ascii(s):
357 return all(ord(c) < 128 for c in s)
358
359
360def _CheckNonAsciiInSourceFiles(input_api, output_api):
361 """Forbids non-ascii characters in source files. """
362
363 def implementation_and_headers(f):
364 return input_api.FilterSourceFile(
Trevor David Blackf69aaf62020-10-05 21:50:10 +0000365 f, files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,))
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600366
367 files_with_non_ascii = []
368 for f in input_api.AffectedSourceFiles(implementation_and_headers):
369 for (num, line) in f.ChangedContents():
370 if not is_ascii(line):
Jamie Madilla89750e2020-04-30 18:00:00 -0400371 files_with_non_ascii.append("%s: %s" % (f, line))
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600372 break
373
374 if files_with_non_ascii:
375 return [
376 output_api.PresubmitError(
377 'Non-ASCII characters in source files.',
378 items=sorted(files_with_non_ascii),
379 long_text='Non-ASCII characters are forbidden in ANGLE source files.\n'
380 'Please remove non-ASCII characters from these files.')
381 ]
382 return []
383
384
Jamie Madill73397e82019-01-09 10:33:16 -0500385def CheckChangeOnUpload(input_api, output_api):
386 results = []
James Darpinian4dff9952019-12-10 17:05:37 -0800387 results.extend(_CheckTabsInSourceFiles(input_api, output_api))
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600388 results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api))
Jamie Madill73397e82019-01-09 10:33:16 -0500389 results.extend(_CheckCodeGeneration(input_api, output_api))
Jamie Madill9e438ee2019-07-05 08:44:23 -0400390 results.extend(_CheckChangeHasBugField(input_api, output_api))
Geoff Langd7d42392019-05-06 13:15:35 -0400391 results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api))
Jamie Madill73397e82019-01-09 10:33:16 -0500392 results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api))
Brian Sheedycc4d8332019-09-11 17:26:00 -0700393 results.extend(_CheckExportValidity(input_api, output_api))
Jamie Madill9e438ee2019-07-05 08:44:23 -0400394 results.extend(
395 input_api.canned_checks.CheckPatchFormatted(
396 input_api, output_api, result_factory=output_api.PresubmitError))
Manh Nguyen2df3bf52020-06-17 17:07:47 -0400397 results.extend(_CheckCommitMessageFormatting(input_api, output_api))
Jamie Madill73397e82019-01-09 10:33:16 -0500398 return results
399
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500400
401def CheckChangeOnCommit(input_api, output_api):
Jamie Madilla89750e2020-04-30 18:00:00 -0400402 return CheckChangeOnUpload(input_api, output_api)