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++ and Objective-C++ implementation files and headers. 18_IMPLEMENTATION_AND_HEADER_EXTENSIONS = r'\.(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 whitelist_strings = ['Revert "', 'Roll '] 45 summary_linelength_warning_lower_limit = 65 46 summary_linelength_warning_upper_limit = 70 47 description_linelength_limit = 72 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 line = commit_msg_lines.pop(0) 121 # lines starting with 4 spaces or lines without space(urls) are exempt from length check 122 if line.startswith(" ") or " " not in line: 123 continue 124 if len(line) > description_linelength_limit: 125 errors.append( 126 output_api.PresubmitError( 127 "Please ensure that your description body is wrapped to " + 128 str(description_linelength_limit) + " characters or less.")) 129 return errors 130 return errors 131 132 133def _CheckChangeHasBugField(input_api, output_api): 134 """Requires that the changelist have a Bug: field from a known project.""" 135 bugs = input_api.change.BugsFromDescription() 136 if not bugs: 137 return [ 138 output_api.PresubmitError('Please ensure that your description contains:\n' 139 '"Bug: angleproject:[bug number]"\n' 140 'directly above the Change-Id tag.') 141 ] 142 143 # The bug must be in the form of "project:number". None is also accepted, which is used by 144 # rollers as well as in very minor changes. 145 if len(bugs) == 1 and bugs[0] == 'None': 146 return [] 147 148 projects = ['angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'b/'] 149 bug_regex = re.compile(r"([a-z]+[:/])(\d+)") 150 errors = [] 151 extra_help = None 152 153 for bug in bugs: 154 if bug == 'None': 155 errors.append( 156 output_api.PresubmitError('Invalid bug tag "None" in presence of other bug tags.')) 157 continue 158 159 match = re.match(bug_regex, bug) 160 if match == None or bug != match.group(0) or match.group(1) not in projects: 161 errors.append(output_api.PresubmitError('Incorrect bug tag "' + bug + '".')) 162 if not extra_help: 163 extra_help = output_api.PresubmitError('Acceptable format is:\n\n' 164 ' Bug: project:bugnumber\n\n' 165 'Acceptable projects are:\n\n ' + 166 '\n '.join(projects)) 167 168 if extra_help: 169 errors.append(extra_help) 170 171 return errors 172 173 174def _CheckCodeGeneration(input_api, output_api): 175 176 class Msg(output_api.PresubmitError): 177 """Specialized error message""" 178 179 def __init__(self, message): 180 super(output_api.PresubmitError, self).__init__( 181 message, 182 long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n' 183 'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n' 184 '\n' 185 'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n' 186 '\n' 187 'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n' 188 'before gclient sync. See the DevSetup documentation for more details.\n') 189 190 code_gen_path = input_api.os_path.join(input_api.PresubmitLocalPath(), 191 'scripts/run_code_generation.py') 192 cmd_name = 'run_code_generation' 193 cmd = [input_api.python_executable, code_gen_path, '--verify-no-dirty'] 194 test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg) 195 if input_api.verbose: 196 print('Running ' + cmd_name) 197 return input_api.RunTests([test_cmd]) 198 199 200# Taken directly from Chromium's PRESUBMIT.py 201def _CheckNewHeaderWithoutGnChange(input_api, output_api): 202 """Checks that newly added header files have corresponding GN changes. 203 Note that this is only a heuristic. To be precise, run script: 204 build/check_gn_headers.py. 205 """ 206 207 def headers(f): 208 return input_api.FilterSourceFile(f, white_list=(r'.+%s' % _HEADER_EXTENSIONS,)) 209 210 new_headers = [] 211 for f in input_api.AffectedSourceFiles(headers): 212 if f.Action() != 'A': 213 continue 214 new_headers.append(f.LocalPath()) 215 216 def gn_files(f): 217 return input_api.FilterSourceFile(f, white_list=(r'.+\.gn',)) 218 219 all_gn_changed_contents = '' 220 for f in input_api.AffectedSourceFiles(gn_files): 221 for _, line in f.ChangedContents(): 222 all_gn_changed_contents += line 223 224 problems = [] 225 for header in new_headers: 226 basename = input_api.os_path.basename(header) 227 if basename not in all_gn_changed_contents: 228 problems.append(header) 229 230 if problems: 231 return [ 232 output_api.PresubmitPromptWarning( 233 'Missing GN changes for new header files', 234 items=sorted(problems), 235 long_text='Please double check whether newly added header files need ' 236 'corresponding changes in gn or gni files.\nThis checking is only a ' 237 'heuristic. Run build/check_gn_headers.py to be precise.\n' 238 'Read https://crbug.com/661774 for more info.') 239 ] 240 return [] 241 242 243def _CheckExportValidity(input_api, output_api): 244 outdir = tempfile.mkdtemp() 245 # shell=True is necessary on Windows, as otherwise subprocess fails to find 246 # either 'gn' or 'vpython3' even if they are findable via PATH. 247 use_shell = input_api.is_windows 248 try: 249 try: 250 subprocess.check_output(['gn', 'gen', outdir], shell=use_shell) 251 except subprocess.CalledProcessError as e: 252 return [ 253 output_api.PresubmitError( 254 'Unable to run gn gen for export_targets.py: %s' % e.output) 255 ] 256 export_target_script = os.path.join(input_api.PresubmitLocalPath(), 'scripts', 257 'export_targets.py') 258 try: 259 subprocess.check_output( 260 ['vpython3', export_target_script, outdir] + _PRIMARY_EXPORT_TARGETS, 261 stderr=subprocess.STDOUT, 262 shell=use_shell) 263 except subprocess.CalledProcessError as e: 264 if input_api.is_committing: 265 return [output_api.PresubmitError('export_targets.py failed: %s' % e.output)] 266 return [ 267 output_api.PresubmitPromptWarning( 268 'export_targets.py failed, this may just be due to your local checkout: %s' % 269 e.output) 270 ] 271 return [] 272 finally: 273 shutil.rmtree(outdir) 274 275 276def _CheckTabsInSourceFiles(input_api, output_api): 277 """Forbids tab characters in source files due to a WebKit repo requirement. """ 278 279 def implementation_and_headers(f): 280 return input_api.FilterSourceFile( 281 f, white_list=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,)) 282 283 files_with_tabs = [] 284 for f in input_api.AffectedSourceFiles(implementation_and_headers): 285 for (num, line) in f.ChangedContents(): 286 if '\t' in line: 287 files_with_tabs.append(f) 288 break 289 290 if files_with_tabs: 291 return [ 292 output_api.PresubmitError( 293 'Tab characters in source files.', 294 items=sorted(files_with_tabs), 295 long_text= 296 'Tab characters are forbidden in ANGLE source files because WebKit\'s Subversion\n' 297 'repository does not allow tab characters in source files.\n' 298 'Please remove tab characters from these files.') 299 ] 300 return [] 301 302 303# https://stackoverflow.com/a/196392 304def is_ascii(s): 305 return all(ord(c) < 128 for c in s) 306 307 308def _CheckNonAsciiInSourceFiles(input_api, output_api): 309 """Forbids non-ascii characters in source files. """ 310 311 def implementation_and_headers(f): 312 return input_api.FilterSourceFile( 313 f, white_list=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,)) 314 315 files_with_non_ascii = [] 316 for f in input_api.AffectedSourceFiles(implementation_and_headers): 317 for (num, line) in f.ChangedContents(): 318 if not is_ascii(line): 319 files_with_non_ascii.append("%s: %s" % (f, line)) 320 break 321 322 if files_with_non_ascii: 323 return [ 324 output_api.PresubmitError( 325 'Non-ASCII characters in source files.', 326 items=sorted(files_with_non_ascii), 327 long_text='Non-ASCII characters are forbidden in ANGLE source files.\n' 328 'Please remove non-ASCII characters from these files.') 329 ] 330 return [] 331 332 333def CheckChangeOnUpload(input_api, output_api): 334 results = [] 335 results.extend(_CheckTabsInSourceFiles(input_api, output_api)) 336 results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api)) 337 results.extend(_CheckCodeGeneration(input_api, output_api)) 338 results.extend(_CheckChangeHasBugField(input_api, output_api)) 339 results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api)) 340 results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api)) 341 results.extend(_CheckExportValidity(input_api, output_api)) 342 results.extend( 343 input_api.canned_checks.CheckPatchFormatted( 344 input_api, output_api, result_factory=output_api.PresubmitError)) 345 results.extend(_CheckCommitMessageFormatting(input_api, output_api)) 346 return results 347 348 349def CheckChangeOnCommit(input_api, output_api): 350 return CheckChangeOnUpload(input_api, output_api) 351