1# 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. 4"""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 10import os 11import re 12import shutil 13import subprocess 14import sys 15import tempfile 16 17# 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)$' 19 20# Fragment of a regular expression that matches C++ and Objective-C++ header files. 21_HEADER_EXTENSIONS = r'\.(h|hpp|hxx)$' 22 23_PRIMARY_EXPORT_TARGETS = [ 24 '//:libEGL', 25 '//:libGLESv1_CM', 26 '//:libGLESv2', 27 '//:translator', 28] 29 30 31def _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 def _IsTagLine(line): 45 return ":" in line 46 47 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 63 allowlist_strings = ['Revert "', 'Roll ', 'Reland ', 'Re-land '] 64 summary_linelength_warning_lower_limit = 65 65 summary_linelength_warning_upper_limit = 70 66 description_linelength_limit = 72 67 68 git_output = input_api.change.DescriptionText() 69 70 multiple_commits = _SplitIntoMultipleCommits(git_output) 71 errors = [] 72 73 for k in range(len(multiple_commits)): 74 commit_msg_lines = multiple_commits[k].splitlines() 75 commit_number = len(multiple_commits) - k 76 commit_tag = "Commit " + str(commit_number) + ":" 77 commit_msg_line_numbers = {} 78 for i in range(len(commit_msg_lines)): 79 commit_msg_line_numbers[commit_msg_lines[i]] = i + 1 80 _PopBlankLines(commit_msg_lines, True) 81 _PopBlankLines(commit_msg_lines, False) 82 allowlisted = False 83 if len(commit_msg_lines) > 0: 84 for allowlist_string in allowlist_strings: 85 if commit_msg_lines[0].startswith(allowlist_string): 86 allowlisted = True 87 break 88 if allowlisted: 89 continue 90 91 if not _CheckTabInCommit(commit_msg_lines): 92 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: 103 errors.append( 104 output_api.PresubmitError( 105 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 177 return errors 178 179 180def _CheckChangeHasBugField(input_api, output_api): 181 """Requires that the changelist have a Bug: field from a known project.""" 182 bugs = input_api.change.BugsFromDescription() 183 if not bugs: 184 return [ 185 output_api.PresubmitError('Please ensure that your description contains:\n' 186 '"Bug: angleproject:[bug number]"\n' 187 'directly above the Change-Id tag.') 188 ] 189 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': 193 return [] 194 195 projects = [ 196 'angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'tint:', 'b/' 197 ] 198 bug_regex = re.compile(r"([a-z]+[:/])(\d+)") 199 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 222 223def _CheckCodeGeneration(input_api, output_api): 224 225 class Msg(output_api.PresubmitError): 226 """Specialized error message""" 227 228 def __init__(self, message): 229 super(output_api.PresubmitError, self).__init__( 230 message, 231 long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n' 232 'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n' 233 '\n' 234 'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n' 235 '\n' 236 'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n' 237 'before gclient sync. See the DevSetup documentation for more details.\n') 238 239 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'] 243 test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg) 244 if input_api.verbose: 245 print('Running ' + cmd_name) 246 return input_api.RunTests([test_cmd]) 247 248 249# Taken directly from Chromium's PRESUBMIT.py 250def _CheckNewHeaderWithoutGnChange(input_api, output_api): 251 """Checks that newly added header files have corresponding GN changes. 252 Note that this is only a heuristic. To be precise, run script: 253 build/check_gn_headers.py. 254 """ 255 256 def headers(f): 257 return input_api.FilterSourceFile(f, files_to_check=(r'.+%s' % _HEADER_EXTENSIONS,)) 258 259 new_headers = [] 260 for f in input_api.AffectedSourceFiles(headers): 261 if f.Action() != 'A': 262 continue 263 new_headers.append(f.LocalPath()) 264 265 def gn_files(f): 266 return input_api.FilterSourceFile(f, files_to_check=(r'.+\.gn',)) 267 268 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 272 273 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) 278 279 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 [] 290 291 292def _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 325def _CheckTabsInSourceFiles(input_api, output_api): 326 """Forbids tab characters in source files due to a WebKit repo requirement. """ 327 328 def implementation_and_headers_including_third_party(f): 329 # Check third_party files too, because WebKit's checks don't make exceptions. 330 return input_api.FilterSourceFile( 331 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]) 334 335 files_with_tabs = [] 336 for f in input_api.AffectedSourceFiles(implementation_and_headers_including_third_party): 337 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 355# 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( 365 f, files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,)) 366 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): 371 files_with_non_ascii.append("%s: %s" % (f, line)) 372 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 385def CheckChangeOnUpload(input_api, output_api): 386 results = [] 387 results.extend(_CheckTabsInSourceFiles(input_api, output_api)) 388 results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api)) 389 results.extend(_CheckCodeGeneration(input_api, output_api)) 390 results.extend(_CheckChangeHasBugField(input_api, output_api)) 391 results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api)) 392 results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api)) 393 results.extend(_CheckExportValidity(input_api, output_api)) 394 results.extend( 395 input_api.canned_checks.CheckPatchFormatted( 396 input_api, output_api, result_factory=output_api.PresubmitError)) 397 results.extend(_CheckCommitMessageFormatting(input_api, output_api)) 398 return results 399 400 401def CheckChangeOnCommit(input_api, output_api): 402 return CheckChangeOnUpload(input_api, output_api) 403