blob: 78cbeb00f00b3433520a16e9128f8ac63bfe5dc3 [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 Darpinian4dff9952019-12-10 17:05:37 -080017# Fragment of a regular expression that matches C++ and Objective-C++ implementation files and headers.
18_IMPLEMENTATION_AND_HEADER_EXTENSIONS = r'\.(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
44 whitelist_strings = ['Revert "', 'Roll ']
45 summary_linelength_warning_lower_limit = 65
46 summary_linelength_warning_upper_limit = 70
47 description_linelength_limit = 71
48
49 if input_api.change.issue:
50 git_output = input_api.gerrit.GetChangeDescription(input_api.change.issue)
51 else:
52 git_output = subprocess.check_output(["git", "log", "-n", "1", "--pretty=format:%B"])
53 commit_msg_lines = git_output.splitlines()
54 _PopBlankLines(commit_msg_lines, True)
55 _PopBlankLines(commit_msg_lines, False)
56 if len(commit_msg_lines) > 0:
57 for whitelist_string in whitelist_strings:
58 if commit_msg_lines[0].startswith(whitelist_string):
59 return []
60 errors = []
61 if git_output.find("\t") != -1:
62 errors.append(output_api.PresubmitError("Tabs are not allowed in commit message."))
63
64 # get rid of the last paragraph, which we assume to always be the tags
65 last_paragraph_line_count = 0
66 while len(commit_msg_lines) > 0 and not _IsLineBlank(commit_msg_lines[-1]):
67 last_paragraph_line_count += 1
68 commit_msg_lines.pop()
69 if last_paragraph_line_count == 0:
70 errors.append(
71 output_api.PresubmitError(
72 "Please ensure that there are tags (e.g., Bug:, Test:) in your description."))
73 if len(commit_msg_lines) > 0:
74 # pop the blank line between tag paragraph and description body
75 commit_msg_lines.pop()
76 if len(commit_msg_lines) > 0 and _IsLineBlank(commit_msg_lines[-1]):
77 errors.append(
78 output_api.PresubmitError('Please ensure that there exists only 1 blank line '
79 'between tags and description body.'))
80 # pop all the remaining blank lines between tag and description body
81 _PopBlankLines(commit_msg_lines, True)
82 if len(commit_msg_lines) == 0:
83 errors.append(
84 output_api.PresubmitError('Please ensure that your description summary'
85 ' and description body are not blank.'))
86 return errors
87
88 if summary_linelength_warning_lower_limit <= len(commit_msg_lines[0]) \
89 <= summary_linelength_warning_upper_limit:
90 errors.append(
91 output_api.PresubmitPromptWarning(
92 "Your description summary should be on one line of " +
93 str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
94 elif len(commit_msg_lines[0]) > summary_linelength_warning_upper_limit:
95 errors.append(
96 output_api.PresubmitError(
97 "Please ensure that your description summary is on one line of " +
98 str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
99 commit_msg_lines.pop(0) # get rid of description summary
100 if len(commit_msg_lines) == 0:
101 return errors
102 if not _IsLineBlank(commit_msg_lines[0]):
103 errors.append(
104 output_api.PresubmitError('Please ensure the summary is only 1 line and '
105 ' there is 1 blank line between the summary '
106 'and description body.'))
107 else:
108 commit_msg_lines.pop(0) # pop first blank line
109 if len(commit_msg_lines) == 0:
110 return errors
111 if _IsLineBlank(commit_msg_lines[0]):
112 errors.append(
113 output_api.PresubmitError('Please ensure that there exists only 1 blank line '
114 'between description summary and description body.'))
115 # pop all the remaining blank lines between description summary and description body
116 _PopBlankLines(commit_msg_lines)
117
118 # loop through description body
119 while len(commit_msg_lines) > 0:
120 if len(commit_msg_lines[0]) > description_linelength_limit:
121 errors.append(
122 output_api.PresubmitError(
123 "Please ensure that your description body is wrapped to " +
124 str(description_linelength_limit) + " characters or less."))
125 return errors
126 commit_msg_lines.pop(0)
127 return errors
128
129
Jamie Madill9e438ee2019-07-05 08:44:23 -0400130def _CheckChangeHasBugField(input_api, output_api):
Shahbaz Youssefi634ee432019-12-16 11:39:10 -0500131 """Requires that the changelist have a Bug: field from a known project."""
Jamie Madill9e438ee2019-07-05 08:44:23 -0400132 bugs = input_api.change.BugsFromDescription()
133 if not bugs:
134 return [
Alexis Hetu4210e492019-10-10 14:22:03 -0400135 output_api.PresubmitError('Please ensure that your description contains:\n'
136 '"Bug: angleproject:[bug number]"\n'
137 'directly above the Change-Id tag.')
Jamie Madill9e438ee2019-07-05 08:44:23 -0400138 ]
Shahbaz Youssefi634ee432019-12-16 11:39:10 -0500139
140 # The bug must be in the form of "project:number". None is also accepted, which is used by
141 # rollers as well as in very minor changes.
142 if len(bugs) == 1 and bugs[0] == 'None':
Jamie Madill9e438ee2019-07-05 08:44:23 -0400143 return []
144
Geoff Lang13e4cdb2020-04-16 14:35:54 -0400145 projects = ['angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'b/']
146 bug_regex = re.compile(r"([a-z]+[:/])(\d+)")
Shahbaz Youssefi634ee432019-12-16 11:39:10 -0500147 errors = []
148 extra_help = None
149
150 for bug in bugs:
151 if bug == 'None':
152 errors.append(
153 output_api.PresubmitError('Invalid bug tag "None" in presence of other bug tags.'))
154 continue
155
156 match = re.match(bug_regex, bug)
157 if match == None or bug != match.group(0) or match.group(1) not in projects:
158 errors.append(output_api.PresubmitError('Incorrect bug tag "' + bug + '".'))
159 if not extra_help:
160 extra_help = output_api.PresubmitError('Acceptable format is:\n\n'
161 ' Bug: project:bugnumber\n\n'
162 'Acceptable projects are:\n\n ' +
163 '\n '.join(projects))
164
165 if extra_help:
166 errors.append(extra_help)
167
168 return errors
169
Jamie Madill9e438ee2019-07-05 08:44:23 -0400170
Jamie Madill73397e82019-01-09 10:33:16 -0500171def _CheckCodeGeneration(input_api, output_api):
Jamie Madill04e9e552019-04-01 14:40:21 -0400172
173 class Msg(output_api.PresubmitError):
Geoff Langd7d42392019-05-06 13:15:35 -0400174 """Specialized error message"""
175
176 def __init__(self, message):
177 super(output_api.PresubmitError, self).__init__(
178 message,
Jamie Madill8dfc2b02019-11-25 15:12:58 -0500179 long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n'
Jamie Madill9e438ee2019-07-05 08:44:23 -0400180 'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n'
181 '\n'
Jamie Madill8dfc2b02019-11-25 15:12:58 -0500182 'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n'
183 '\n'
Jamie Madill9e438ee2019-07-05 08:44:23 -0400184 'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n'
185 'before gclient sync. See the DevSetup documentation for more details.\n')
Jamie Madill04e9e552019-04-01 14:40:21 -0400186
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500187 code_gen_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
188 'scripts/run_code_generation.py')
189 cmd_name = 'run_code_generation'
190 cmd = [input_api.python_executable, code_gen_path, '--verify-no-dirty']
Geoff Langd7d42392019-05-06 13:15:35 -0400191 test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg)
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500192 if input_api.verbose:
193 print('Running ' + cmd_name)
194 return input_api.RunTests([test_cmd])
195
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500196
Jamie Madill73397e82019-01-09 10:33:16 -0500197# Taken directly from Chromium's PRESUBMIT.py
198def _CheckNewHeaderWithoutGnChange(input_api, output_api):
Geoff Langd7d42392019-05-06 13:15:35 -0400199 """Checks that newly added header files have corresponding GN changes.
Jamie Madill73397e82019-01-09 10:33:16 -0500200 Note that this is only a heuristic. To be precise, run script:
201 build/check_gn_headers.py.
202 """
203
Geoff Langd7d42392019-05-06 13:15:35 -0400204 def headers(f):
205 return input_api.FilterSourceFile(f, white_list=(r'.+%s' % _HEADER_EXTENSIONS,))
Jamie Madill73397e82019-01-09 10:33:16 -0500206
Geoff Langd7d42392019-05-06 13:15:35 -0400207 new_headers = []
208 for f in input_api.AffectedSourceFiles(headers):
209 if f.Action() != 'A':
210 continue
211 new_headers.append(f.LocalPath())
Jamie Madill73397e82019-01-09 10:33:16 -0500212
Geoff Langd7d42392019-05-06 13:15:35 -0400213 def gn_files(f):
214 return input_api.FilterSourceFile(f, white_list=(r'.+\.gn',))
Jamie Madill73397e82019-01-09 10:33:16 -0500215
Geoff Langd7d42392019-05-06 13:15:35 -0400216 all_gn_changed_contents = ''
217 for f in input_api.AffectedSourceFiles(gn_files):
218 for _, line in f.ChangedContents():
219 all_gn_changed_contents += line
Jamie Madill73397e82019-01-09 10:33:16 -0500220
Geoff Langd7d42392019-05-06 13:15:35 -0400221 problems = []
222 for header in new_headers:
223 basename = input_api.os_path.basename(header)
224 if basename not in all_gn_changed_contents:
225 problems.append(header)
Jamie Madill73397e82019-01-09 10:33:16 -0500226
Geoff Langd7d42392019-05-06 13:15:35 -0400227 if problems:
228 return [
229 output_api.PresubmitPromptWarning(
230 'Missing GN changes for new header files',
231 items=sorted(problems),
232 long_text='Please double check whether newly added header files need '
233 'corresponding changes in gn or gni files.\nThis checking is only a '
234 'heuristic. Run build/check_gn_headers.py to be precise.\n'
235 'Read https://crbug.com/661774 for more info.')
236 ]
237 return []
Jamie Madill73397e82019-01-09 10:33:16 -0500238
239
Brian Sheedycc4d8332019-09-11 17:26:00 -0700240def _CheckExportValidity(input_api, output_api):
241 outdir = tempfile.mkdtemp()
242 # shell=True is necessary on Windows, as otherwise subprocess fails to find
243 # either 'gn' or 'vpython3' even if they are findable via PATH.
244 use_shell = input_api.is_windows
245 try:
246 try:
247 subprocess.check_output(['gn', 'gen', outdir], shell=use_shell)
248 except subprocess.CalledProcessError as e:
249 return [
250 output_api.PresubmitError(
251 'Unable to run gn gen for export_targets.py: %s' % e.output)
252 ]
253 export_target_script = os.path.join(input_api.PresubmitLocalPath(), 'scripts',
254 'export_targets.py')
255 try:
256 subprocess.check_output(
257 ['vpython3', export_target_script, outdir] + _PRIMARY_EXPORT_TARGETS,
258 stderr=subprocess.STDOUT,
259 shell=use_shell)
260 except subprocess.CalledProcessError as e:
261 if input_api.is_committing:
262 return [output_api.PresubmitError('export_targets.py failed: %s' % e.output)]
263 return [
264 output_api.PresubmitPromptWarning(
265 'export_targets.py failed, this may just be due to your local checkout: %s' %
266 e.output)
267 ]
268 return []
269 finally:
270 shutil.rmtree(outdir)
271
272
James Darpinian4dff9952019-12-10 17:05:37 -0800273def _CheckTabsInSourceFiles(input_api, output_api):
274 """Forbids tab characters in source files due to a WebKit repo requirement. """
275
276 def implementation_and_headers(f):
277 return input_api.FilterSourceFile(
278 f, white_list=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,))
279
280 files_with_tabs = []
281 for f in input_api.AffectedSourceFiles(implementation_and_headers):
282 for (num, line) in f.ChangedContents():
283 if '\t' in line:
284 files_with_tabs.append(f)
285 break
286
287 if files_with_tabs:
288 return [
289 output_api.PresubmitError(
290 'Tab characters in source files.',
291 items=sorted(files_with_tabs),
292 long_text=
293 'Tab characters are forbidden in ANGLE source files because WebKit\'s Subversion\n'
294 'repository does not allow tab characters in source files.\n'
295 'Please remove tab characters from these files.')
296 ]
297 return []
298
299
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600300# https://stackoverflow.com/a/196392
301def is_ascii(s):
302 return all(ord(c) < 128 for c in s)
303
304
305def _CheckNonAsciiInSourceFiles(input_api, output_api):
306 """Forbids non-ascii characters in source files. """
307
308 def implementation_and_headers(f):
309 return input_api.FilterSourceFile(
310 f, white_list=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,))
311
312 files_with_non_ascii = []
313 for f in input_api.AffectedSourceFiles(implementation_and_headers):
314 for (num, line) in f.ChangedContents():
315 if not is_ascii(line):
Jamie Madilla89750e2020-04-30 18:00:00 -0400316 files_with_non_ascii.append("%s: %s" % (f, line))
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600317 break
318
319 if files_with_non_ascii:
320 return [
321 output_api.PresubmitError(
322 'Non-ASCII characters in source files.',
323 items=sorted(files_with_non_ascii),
324 long_text='Non-ASCII characters are forbidden in ANGLE source files.\n'
325 'Please remove non-ASCII characters from these files.')
326 ]
327 return []
328
329
Jamie Madill73397e82019-01-09 10:33:16 -0500330def CheckChangeOnUpload(input_api, output_api):
331 results = []
James Darpinian4dff9952019-12-10 17:05:37 -0800332 results.extend(_CheckTabsInSourceFiles(input_api, output_api))
Tim Van Pattenf7d44732020-04-29 16:55:01 -0600333 results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api))
Jamie Madill73397e82019-01-09 10:33:16 -0500334 results.extend(_CheckCodeGeneration(input_api, output_api))
Jamie Madill9e438ee2019-07-05 08:44:23 -0400335 results.extend(_CheckChangeHasBugField(input_api, output_api))
Geoff Langd7d42392019-05-06 13:15:35 -0400336 results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api))
Jamie Madill73397e82019-01-09 10:33:16 -0500337 results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api))
Brian Sheedycc4d8332019-09-11 17:26:00 -0700338 results.extend(_CheckExportValidity(input_api, output_api))
Jamie Madill9e438ee2019-07-05 08:44:23 -0400339 results.extend(
340 input_api.canned_checks.CheckPatchFormatted(
341 input_api, output_api, result_factory=output_api.PresubmitError))
Manh Nguyenebc6d0a2020-05-27 18:21:30 -0400342 results.extend(_CheckCommitMessageFormatting(input_api, output_api))
Jamie Madill73397e82019-01-09 10:33:16 -0500343 return results
344
Shahbaz Youssefi98a35502019-01-08 22:09:39 -0500345
346def CheckChangeOnCommit(input_api, output_api):
Jamie Madilla89750e2020-04-30 18:00:00 -0400347 return CheckChangeOnUpload(input_api, output_api)