• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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