blob: a28e0e0925533e3c96a157bcf0ff6ef0d3ab32dc [file] [log] [blame]
Kevin Lubick7f7b6ab2021-08-16 15:00:15 -04001#!/usr/bin/env python3
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +00002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6
7"""Top-level presubmit script for Skia.
8
9See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
10for more details about the presubmit API built into gcl.
11"""
12
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000013import fnmatch
rmistry@google.comf6c5f752013-03-29 17:26:00 +000014import os
commit-bot@chromium.orgcfdc5962014-01-31 17:33:04 +000015import re
rmistryd223fb22015-02-26 10:16:13 -080016import subprocess
rmistry@google.comf6c5f752013-03-29 17:26:00 +000017import sys
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000018import traceback
rmistry@google.comf6c5f752013-03-29 17:26:00 +000019
rmistry@google.comc2993442013-01-23 14:35:58 +000020
Chris Mumfordbe879e02023-04-24 09:07:20 -070021RELEASE_NOTES_DIR = 'relnotes'
Chris Mumfordb0dbd9e2023-03-29 08:15:18 -070022RELEASE_NOTES_FILE_NAME = 'RELEASE_NOTES.md'
Chris Mumfordbe879e02023-04-24 09:07:20 -070023RELEASE_NOTES_README = '//relnotes/README.md'
commit-bot@chromium.org745e08c2014-02-03 14:18:32 +000024
rmistryd88b0be2016-05-20 03:50:01 -070025GOLD_TRYBOT_URL = 'https://gold.skia.org/search?issue='
rmistryd223fb22015-02-26 10:16:13 -080026
Eric Boren1eec99c2018-04-26 13:09:48 -040027SERVICE_ACCOUNT_SUFFIX = [
Eric Boren47ed6f12018-04-26 14:02:43 -040028 '@%s.iam.gserviceaccount.com' % project for project in [
Eric Boren6ad3ca42018-09-07 14:22:16 -040029 'skia-buildbots.google.com', 'skia-swarming-bots', 'skia-public',
Ravi Mistry53c44232019-03-12 08:51:42 -040030 'skia-corp.google.com', 'chops-service-accounts']]
Eric Borendd988292018-01-02 13:29:21 -050031
Kevin Lubick2f576262022-05-06 08:30:28 -040032USE_PYTHON3 = True
33
rmistry@google.com547012d2013-04-12 19:45:46 +000034
rmistry@google.com713276b2013-01-25 18:27:34 +000035def _CheckChangeHasEol(input_api, output_api, source_file_filter=None):
Edward Lemur2b7876c2020-01-17 18:48:13 -050036 """Checks that files end with at least one \n (LF)."""
rmistry@google.com713276b2013-01-25 18:27:34 +000037 eof_files = []
38 for f in input_api.AffectedSourceFiles(source_file_filter):
39 contents = input_api.ReadFile(f, 'rb')
Edward Lemur2b7876c2020-01-17 18:48:13 -050040 # Check that the file ends in at least one newline character.
rmistry@google.com713276b2013-01-25 18:27:34 +000041 if len(contents) > 1 and contents[-1:] != '\n':
42 eof_files.append(f.LocalPath())
43
44 if eof_files:
45 return [output_api.PresubmitPromptWarning(
46 'These files should end in a newline character:',
47 items=eof_files)]
48 return []
49
50
Ben Wagnercf42e982018-02-09 17:41:20 -050051def _JsonChecks(input_api, output_api):
52 """Run checks on any modified json files."""
53 failing_files = []
54 for affected_file in input_api.AffectedFiles(None):
55 affected_file_path = affected_file.LocalPath()
56 is_json = affected_file_path.endswith('.json')
57 is_metadata = (affected_file_path.startswith('site/') and
58 affected_file_path.endswith('/METADATA'))
59 if is_json or is_metadata:
60 try:
61 input_api.json.load(open(affected_file_path, 'r'))
62 except ValueError:
63 failing_files.append(affected_file_path)
64
65 results = []
66 if failing_files:
67 results.append(
68 output_api.PresubmitError(
69 'The following files contain invalid json:\n%s\n\n' %
70 '\n'.join(failing_files)))
71 return results
72
73
rmistry01cbf6c2015-03-12 07:48:40 -070074def _IfDefChecks(input_api, output_api):
75 """Ensures if/ifdef are not before includes. See skbug/3362 for details."""
76 comment_block_start_pattern = re.compile('^\s*\/\*.*$')
77 comment_block_middle_pattern = re.compile('^\s+\*.*')
78 comment_block_end_pattern = re.compile('^\s+\*\/.*$')
79 single_line_comment_pattern = re.compile('^\s*//.*$')
80 def is_comment(line):
81 return (comment_block_start_pattern.match(line) or
82 comment_block_middle_pattern.match(line) or
83 comment_block_end_pattern.match(line) or
84 single_line_comment_pattern.match(line))
85
86 empty_line_pattern = re.compile('^\s*$')
87 def is_empty_line(line):
88 return empty_line_pattern.match(line)
89
90 failing_files = []
91 for affected_file in input_api.AffectedSourceFiles(None):
92 affected_file_path = affected_file.LocalPath()
93 if affected_file_path.endswith('.cpp') or affected_file_path.endswith('.h'):
94 f = open(affected_file_path)
James Godfrey-Kittle5d553ed2022-05-06 14:06:40 -040095 for line in f:
rmistry01cbf6c2015-03-12 07:48:40 -070096 if is_comment(line) or is_empty_line(line):
97 continue
98 # The below will be the first real line after comments and newlines.
99 if line.startswith('#if 0 '):
100 pass
101 elif line.startswith('#if ') or line.startswith('#ifdef '):
102 failing_files.append(affected_file_path)
103 break
104
105 results = []
106 if failing_files:
107 results.append(
108 output_api.PresubmitError(
109 'The following files have #if or #ifdef before includes:\n%s\n\n'
halcanary6950de62015-11-07 05:29:00 -0800110 'See https://bug.skia.org/3362 for why this should be fixed.' %
rmistry01cbf6c2015-03-12 07:48:40 -0700111 '\n'.join(failing_files)))
112 return results
113
114
borenetc7c91802015-03-25 04:47:02 -0700115def _CopyrightChecks(input_api, output_api, source_file_filter=None):
116 results = []
117 year_pattern = r'\d{4}'
118 year_range_pattern = r'%s(-%s)?' % (year_pattern, year_pattern)
119 years_pattern = r'%s(,%s)*,?' % (year_range_pattern, year_range_pattern)
120 copyright_pattern = (
121 r'Copyright (\([cC]\) )?%s \w+' % years_pattern)
122
123 for affected_file in input_api.AffectedSourceFiles(source_file_filter):
John Stilesd836f842020-09-14 10:21:44 -0400124 if ('third_party/' in affected_file.LocalPath() or
Kevin Lubickfed97e82022-03-25 14:59:33 -0400125 'tests/sksl/' in affected_file.LocalPath() or
Kevin Lubick1b1e64c2022-07-18 13:30:16 -0400126 'bazel/rbe/' in affected_file.LocalPath() or
Chris Mumford4c967b72022-08-02 09:31:02 -0700127 'bazel/external/' in affected_file.LocalPath() or
128 'bazel/exporter/interfaces/mocks/' in affected_file.LocalPath()):
borenetc7c91802015-03-25 04:47:02 -0700129 continue
130 contents = input_api.ReadFile(affected_file, 'rb')
131 if not re.search(copyright_pattern, contents):
132 results.append(output_api.PresubmitError(
133 '%s is missing a correct copyright header.' % affected_file))
134 return results
135
136
borenet2dbbfa52016-10-14 06:32:09 -0700137def _InfraTests(input_api, output_api):
138 """Run the infra tests."""
borenet1ed2ae42016-07-26 11:52:17 -0700139 results = []
mtklein3da80f52016-07-27 04:14:07 -0700140 if not any(f.LocalPath().startswith('infra')
141 for f in input_api.AffectedFiles()):
142 return results
143
James Godfrey-Kittle5d553ed2022-05-06 14:06:40 -0400144 cmd = ['python3', os.path.join('infra', 'bots', 'infra_tests.py')]
borenet60b0a2d2016-10-04 12:45:41 -0700145 try:
146 subprocess.check_output(cmd)
147 except subprocess.CalledProcessError as e:
148 results.append(output_api.PresubmitError(
149 '`%s` failed:\n%s' % (' '.join(cmd), e.output)))
150 return results
151
152
mtklein4db3b792016-08-03 14:18:22 -0700153def _CheckGNFormatted(input_api, output_api):
154 """Make sure any .gn files we're changing have been formatted."""
Ben Wagner3c4a9d32020-02-14 14:28:33 -0500155 files = []
Corentin Wallez6a5187a2020-04-08 10:24:04 +0200156 for f in input_api.AffectedFiles(include_deletes=False):
Ben Wagner3c4a9d32020-02-14 14:28:33 -0500157 if (f.LocalPath().endswith('.gn') or
158 f.LocalPath().endswith('.gni')):
159 files.append(f)
160 if not files:
161 return []
mtklein4db3b792016-08-03 14:18:22 -0700162
James Godfrey-Kittle5d553ed2022-05-06 14:06:40 -0400163 cmd = ['python3', os.path.join('bin', 'fetch-gn')]
Ben Wagner3c4a9d32020-02-14 14:28:33 -0500164 try:
165 subprocess.check_output(cmd)
166 except subprocess.CalledProcessError as e:
167 return [output_api.PresubmitError(
168 '`%s` failed:\n%s' % (' '.join(cmd), e.output))]
169
170 results = []
171 for f in files:
Brian Osman70f24af2020-02-18 15:08:27 -0500172 gn = 'gn.exe' if 'win32' in sys.platform else 'gn'
Ben Wagner06265e02020-02-13 19:02:46 -0500173 gn = os.path.join(input_api.PresubmitLocalPath(), 'bin', gn)
Mike Klein7a1c53d2016-10-11 14:03:06 -0400174 cmd = [gn, 'format', '--dry-run', f.LocalPath()]
mtklein4db3b792016-08-03 14:18:22 -0700175 try:
176 subprocess.check_output(cmd)
177 except subprocess.CalledProcessError:
Ben Wagner06265e02020-02-13 19:02:46 -0500178 fix = 'bin/gn format ' + f.LocalPath()
mtklein4db3b792016-08-03 14:18:22 -0700179 results.append(output_api.PresubmitError(
mtkleind434b012016-08-10 07:30:58 -0700180 '`%s` failed, try\n\t%s' % (' '.join(cmd), fix)))
mtklein4db3b792016-08-03 14:18:22 -0700181 return results
182
Ravi Mistry6eca5792020-12-16 11:42:29 -0500183
184def _CheckGitConflictMarkers(input_api, output_api):
185 pattern = input_api.re.compile('^(?:<<<<<<<|>>>>>>>) |^=======$')
186 results = []
187 for f in input_api.AffectedFiles():
188 for line_num, line in f.ChangedContents():
189 if f.LocalPath().endswith('.md'):
190 # First-level headers in markdown look a lot like version control
191 # conflict markers. http://daringfireball.net/projects/markdown/basics
192 continue
193 if pattern.match(line):
194 results.append(
195 output_api.PresubmitError(
196 'Git conflict markers found in %s:%d %s' % (
197 f.LocalPath(), line_num, line)))
198 return results
199
200
Mike Kleinbb413432019-07-26 11:55:40 -0500201def _CheckIncludesFormatted(input_api, output_api):
202 """Make sure #includes in files we're changing have been formatted."""
Mike Kleinf9ad5ba2019-07-29 12:34:39 -0500203 files = [str(f) for f in input_api.AffectedFiles() if f.Action() != 'D']
James Godfrey-Kittle5d553ed2022-05-06 14:06:40 -0400204 cmd = ['python3',
Mike Kleinbb413432019-07-26 11:55:40 -0500205 'tools/rewrite_includes.py',
Mike Kleinf9ad5ba2019-07-29 12:34:39 -0500206 '--dry-run'] + files
Hal Canary4df3d532019-07-30 13:49:45 -0400207 if 0 != subprocess.call(cmd):
Mike Kleinbb413432019-07-26 11:55:40 -0500208 return [output_api.PresubmitError('`%s` failed' % ' '.join(cmd))]
209 return []
borenet1ed2ae42016-07-26 11:52:17 -0700210
Eric Boren58d1f762019-07-19 08:07:44 -0400211
Ben Wagner88855502017-10-12 17:55:19 -0400212class _WarningsAsErrors():
213 def __init__(self, output_api):
214 self.output_api = output_api
215 self.old_warning = None
216 def __enter__(self):
217 self.old_warning = self.output_api.PresubmitPromptWarning
218 self.output_api.PresubmitPromptWarning = self.output_api.PresubmitError
219 return self.output_api
220 def __exit__(self, ex_type, ex_value, ex_traceback):
221 self.output_api.PresubmitPromptWarning = self.old_warning
222
223
Kevin Lubick2cd80672021-07-01 11:03:36 -0400224def _RegenerateAllExamplesCPP(input_api, output_api):
225 """Regenerates all_examples.cpp if an example was added or deleted."""
226 if not any(f.LocalPath().startswith('docs/examples/')
227 for f in input_api.AffectedFiles()):
228 return []
229 command_str = 'tools/fiddle/make_all_examples_cpp.py'
James Godfrey-Kittle5d553ed2022-05-06 14:06:40 -0400230 cmd = ['python3', command_str]
Kevin Lubick2cd80672021-07-01 11:03:36 -0400231 if 0 != subprocess.call(cmd):
232 return [output_api.PresubmitError('`%s` failed' % ' '.join(cmd))]
233
234 results = []
235 git_diff_output = input_api.subprocess.check_output(
236 ['git', 'diff', '--no-ext-diff'])
237 if git_diff_output:
238 results += [output_api.PresubmitError(
239 'Diffs found after running "%s":\n\n%s\n'
240 'Please commit or discard the above changes.' % (
241 command_str,
242 git_diff_output,
243 )
244 )]
245 return results
246
Chris Mumford6f6793b2022-12-01 07:19:27 -0800247
Kevin Lubickd7dc6d72023-02-13 09:28:29 -0500248def _CheckExamplesForPrivateAPIs(input_api, output_api):
249 """We only want our checked-in examples (aka fiddles) to show public API."""
250 banned_includes = [
251 input_api.re.compile(r'#\s*include\s+("src/.*)'),
252 input_api.re.compile(r'#\s*include\s+("include/private/.*)'),
253 ]
254 file_filter = lambda x: (x.LocalPath().startswith('docs/examples/'))
255 errors = []
256 for affected_file in input_api.AffectedSourceFiles(file_filter):
257 affected_filepath = affected_file.LocalPath()
258 for (line_num, line) in affected_file.ChangedContents():
259 for re in banned_includes:
260 match = re.search(line)
261 if match:
262 errors.append('%s:%s: Fiddles should not use private/internal API like %s.' % (
263 affected_filepath, line_num, match.group(1)))
264
265 if errors:
266 return [output_api.PresubmitError('\n'.join(errors))]
267 return []
268
269
Chris Mumford6f6793b2022-12-01 07:19:27 -0800270def _CheckGeneratedBazelBUILDFiles(input_api, output_api):
271 if 'win32' in sys.platform:
272 # TODO(crbug.com/skia/12541): Remove when Bazel builds work on Windows.
273 # Note: `make` is not installed on Windows by default.
274 return []
Kevin Lubickd5687052022-12-02 10:25:26 -0500275 if 'darwin' in sys.platform:
276 # This takes too long on Mac with default settings. Probably due to sandboxing.
277 return []
Chris Mumford6f6793b2022-12-01 07:19:27 -0800278 for affected_file in input_api.AffectedFiles(include_deletes=True):
279 affected_file_path = affected_file.LocalPath()
280 if (affected_file_path.endswith('.go') or
281 affected_file_path.endswith('BUILD.bazel')):
282 return _RunCommandAndCheckGitDiff(output_api,
283 ['make', '-C', 'bazel', 'generate_go'])
284 return [] # No modified Go source files.
285
286
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400287def _CheckBazelBUILDFiles(input_api, output_api):
288 """Makes sure our BUILD.bazel files are compatible with G3."""
289 results = []
Kevin Lubick269e6872022-05-04 11:06:10 -0400290 for affected_file in input_api.AffectedFiles(include_deletes=False):
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400291 affected_file_path = affected_file.LocalPath()
292 is_bazel = affected_file_path.endswith('BUILD.bazel')
Kevin Lubick3413ca42022-05-06 13:20:12 -0400293 # This list lines up with the one in autoroller_lib.py (see G3).
Kevin Lubickc123b5a2022-05-27 15:12:31 -0400294 excluded_paths = ["infra/", "bazel/rbe/", "bazel/external/", "bazel/common_config_settings/",
Kevin Lubick12a33362022-10-05 16:47:13 -0400295 "modules/canvaskit/go/", "experimental/", "bazel/platform", "third_party/",
Kevin Lubickbcfa3ef2022-11-07 10:18:09 -0500296 "tests/", "resources/", "bazel/deps_parser/", "bazel/exporter_tool/",
Kevin Lubickb80e74d2023-05-25 08:18:17 -0400297 "tools/gpu/gl/interface/", "bazel/utils/", "include/config/",
Kevin Lubick6f4fd972023-08-29 10:44:24 -0400298 "bench/", "example/external_client/"]
Kevin Lubick3413ca42022-05-06 13:20:12 -0400299 is_excluded = any(affected_file_path.startswith(n) for n in excluded_paths)
300 if is_bazel and not is_excluded:
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400301 with open(affected_file_path, 'r') as file:
302 contents = file.read()
Kevin Lubickee62fad2022-06-01 14:45:46 -0400303 if 'exports_files_legacy(' not in contents:
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400304 results.append(output_api.PresubmitError(
305 ('%s needs to call exports_files_legacy() to support legacy G3 ' +
306 'rules.\nPut this near the top of the file, beneath ' +
307 'licenses(["notice"]).') % affected_file_path
308 ))
309 if 'licenses(["notice"])' not in contents:
310 results.append(output_api.PresubmitError(
311 ('%s needs to have\nlicenses(["notice"])\nimmediately after ' +
312 'the load() calls to comply with G3 policies.') % affected_file_path
313 ))
Kevin Lubick25a8f602022-07-28 16:03:56 -0400314 if 'cc_library(' in contents and '"skia_cc_library"' not in contents:
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400315 results.append(output_api.PresubmitError(
Kevin Lubick25a8f602022-07-28 16:03:56 -0400316 ('%s needs to load skia_cc_library from macros.bzl instead of using the ' +
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400317 'native one. This allows us to build differently for G3.\n' +
Kevin Lubick25a8f602022-07-28 16:03:56 -0400318 'Add "skia_cc_library" to load("//bazel:macros.bzl", ...)')
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400319 % affected_file_path
320 ))
321 return results
322
Kevin Lubick8d9d9fa2022-05-17 13:43:52 -0400323
324def _CheckPublicBzl(input_api, output_api):
325 """Reminds devs to add/remove files from public.bzl."""
326 results = []
327 public_bzl = ''
328 with open('public.bzl', 'r', encoding='utf-8') as f:
329 public_bzl = f.read().strip()
330 for affected_file in input_api.AffectedFiles(include_deletes=True):
331 # action is A for newly added, D for newly deleted, M for modified
332 action = affected_file.Action()
333 affected_file_path = affected_file.LocalPath()
334 if ((affected_file_path.startswith("include") or affected_file_path.startswith("src")) and
335 (affected_file_path.endswith(".cpp") or affected_file_path.endswith(".h"))):
336 affected_file_path = '"' + affected_file_path + '"'
337 if action == "D" and affected_file_path in public_bzl:
338 results.append(output_api.PresubmitError(
339 "Need to delete %s from public.bzl (or rename it)" % affected_file_path))
340 elif action == "A" and affected_file_path not in public_bzl:
341 results.append(output_api.PresubmitPromptWarning(
342 "You may need to add %s to public.bzl" % affected_file_path))
343 return results
344
345
Kevin Lubick2c655792022-05-27 13:56:03 -0400346def _RunCommandAndCheckGitDiff(output_api, command):
347 """Run an arbitrary command. Fail if it produces any diffs."""
348 command_str = ' '.join(command)
349 results = []
350
351 try:
352 output = subprocess.check_output(
353 command,
354 stderr=subprocess.STDOUT, encoding='utf-8')
355 except subprocess.CalledProcessError as e:
356 results += [output_api.PresubmitError(
357 'Command "%s" returned non-zero exit code %d. Output: \n\n%s' % (
358 command_str,
359 e.returncode,
360 e.output,
361 )
362 )]
363
364 git_diff_output = subprocess.check_output(
365 ['git', 'diff', '--no-ext-diff'], encoding='utf-8')
366 if git_diff_output:
367 results += [output_api.PresubmitError(
368 'Diffs found after running "%s":\n\n%s\n'
369 'Please commit or discard the above changes.' % (
370 command_str,
371 git_diff_output,
372 )
373 )]
374
375 return results
376
377
Chris Mumford008981f2022-12-01 08:29:24 -0800378def _CheckGNIGenerated(input_api, output_api):
379 """Ensures that the generated *.gni files are current.
380
381 The Bazel project files are authoritative and some *.gni files are
382 generated from them using the exporter_tool. This check ensures they
383 are still current.
384 """
385 if 'win32' in sys.platform:
386 # TODO(crbug.com/skia/12541): Remove when Bazel builds work on Windows.
387 # Note: `make` is not installed on Windows by default.
388 return [
389 output_api.PresubmitPromptWarning(
390 'Skipping Bazel=>GNI export check on Windows (unsupported platform).'
391 )
392 ]
Kevin Lubickad28b6c2022-12-02 13:42:17 -0500393 if 'darwin' in sys.platform:
394 # This takes too long on Mac with default settings. Probably due to sandboxing.
395 return []
Kevin Lubicka2630de2023-03-13 16:15:27 -0400396 should_run = False
Chris Mumford008981f2022-12-01 08:29:24 -0800397 for affected_file in input_api.AffectedFiles(include_deletes=True):
398 affected_file_path = affected_file.LocalPath()
399 if affected_file_path.endswith('BUILD.bazel') or affected_file_path.endswith('.gni'):
Kevin Lubicka2630de2023-03-13 16:15:27 -0400400 should_run = True
401 # Generate GNI files and verify no changes.
402 if should_run:
403 return _RunCommandAndCheckGitDiff(output_api,
404 ['make', '-C', 'bazel', 'generate_gni'])
Chris Mumford008981f2022-12-01 08:29:24 -0800405
406 # No Bazel build files changed.
407 return []
408
409
Kevin Lubick2c655792022-05-27 13:56:03 -0400410def _CheckBuildifier(input_api, output_api):
411 """Runs Buildifier and fails on linting errors, or if it produces any diffs.
412
413 This check only runs if the affected files include any WORKSPACE, BUILD,
414 BUILD.bazel or *.bzl files.
415 """
416 files = []
Leandro Lovisolo4c099aa2023-11-15 00:22:21 +0000417 # Please keep the below exclude patterns in sync with those in the //:buildifier rule definition.
Kevin Lubick2c655792022-05-27 13:56:03 -0400418 for affected_file in input_api.AffectedFiles(include_deletes=False):
419 affected_file_path = affected_file.LocalPath()
420 if affected_file_path.endswith('BUILD.bazel') or affected_file_path.endswith('.bzl'):
Leandro Lovisolo17b801d2023-08-14 18:20:58 +0000421 if not affected_file_path.endswith('public.bzl') and \
422 not affected_file_path.endswith('go_repositories.bzl') and \
Leandro Lovisolo4c099aa2023-11-15 00:22:21 +0000423 not "bazel/rbe/gce_linux/" in affected_file_path and \
424 not affected_file_path.startswith("third_party/externals/") and \
425 not "node_modules/" in affected_file_path: # Skip generated files.
Kevin Lubickd0519f12022-06-02 15:42:58 -0400426 files.append(affected_file_path)
Kevin Lubick2c655792022-05-27 13:56:03 -0400427 if not files:
428 return []
429 try:
430 subprocess.check_output(
431 ['buildifier', '--version'],
432 stderr=subprocess.STDOUT)
Kevin Lubickd39593d2022-06-02 15:22:29 -0400433 except:
434 return [output_api.PresubmitNotifyResult(
Kevin Lubick2c655792022-05-27 13:56:03 -0400435 'Skipping buildifier check because it is not on PATH. \n' +
Kevin Lubickd39593d2022-06-02 15:22:29 -0400436 'You can download it from https://github.com/bazelbuild/buildtools/releases')]
Kevin Lubick2c655792022-05-27 13:56:03 -0400437
438 return _RunCommandAndCheckGitDiff(
439 # One can change --lint=warn to --lint=fix to have things automatically fixed where possible.
440 # However, --lint=fix will not cause a presubmit error if there are things that require
441 # manual intervention, so we leave --lint=warn on by default.
Leandro Lovisolo4c099aa2023-11-15 00:22:21 +0000442 #
443 # Please keep the below arguments in sync with those in the //:buildifier rule definition.
444 output_api, [
445 'buildifier',
446 '--mode=fix',
447 '--lint=warn',
448 '--warnings',
449 ','.join([
450 '-native-android',
451 '-native-cc',
452 '-native-py',
453 ])
454 ] + files)
Kevin Lubick2c655792022-05-27 13:56:03 -0400455
456
Kevin Lubick07e7bc12022-10-03 09:18:35 -0400457def _CheckBannedAPIs(input_api, output_api):
458 """Check source code for functions and packages that should not be used."""
459
460 # A list of tuples of a regex to match an API and a suggested replacement for
461 # that API. There is an optional third parameter for files which *can* use this
462 # API without warning.
463 banned_replacements = [
464 (r'std::stof\(', 'std::strtof(), which does not throw'),
465 (r'std::stod\(', 'std::strtod(), which does not throw'),
466 (r'std::stold\(', 'std::strtold(), which does not throw'),
467 ]
468
Kevin Lubick7f92cf82023-02-28 14:18:00 -0500469 # These defines are either there or not, and using them with just an #if is a
470 # subtle, frustrating bug.
John Stilese9e4d0d2023-08-28 18:31:47 -0400471 existence_defines = ['SK_GANESH', 'SK_GRAPHITE', 'SK_GL', 'SK_VULKAN', 'SK_DAWN', 'SK_METAL',
472 'SK_DIRECT3D', 'SK_DEBUG', 'GR_TEST_UTILS', 'GRAPHITE_TEST_UTILS']
Kevin Lubick7f92cf82023-02-28 14:18:00 -0500473 for d in existence_defines:
474 banned_replacements.append(('#if {}'.format(d),
475 '#if defined({})'.format(d)))
Kevin Lubick07e7bc12022-10-03 09:18:35 -0400476 compiled_replacements = []
477 for rep in banned_replacements:
478 exceptions = []
479 if len(rep) == 3:
480 (re, replacement, exceptions) = rep
481 else:
482 (re, replacement) = rep
483
484 compiled_re = input_api.re.compile(re)
485 compiled_exceptions = [input_api.re.compile(exc) for exc in exceptions]
486 compiled_replacements.append(
487 (compiled_re, replacement, compiled_exceptions))
488
489 errors = []
490 file_filter = lambda x: (x.LocalPath().endswith('.h') or
491 x.LocalPath().endswith('.cpp') or
492 x.LocalPath().endswith('.cc') or
493 x.LocalPath().endswith('.m') or
494 x.LocalPath().endswith('.mm'))
495 for affected_file in input_api.AffectedSourceFiles(file_filter):
496 affected_filepath = affected_file.LocalPath()
497 for (line_num, line) in affected_file.ChangedContents():
498 for (re, replacement, exceptions) in compiled_replacements:
499 match = re.search(line)
500 if match:
501 for exc in exceptions:
502 if exc.search(affected_filepath):
503 break
504 else:
505 errors.append('%s:%s: Instead of %s, please use %s.' % (
506 affected_filepath, line_num, match.group(), replacement))
507
508 if errors:
509 return [output_api.PresubmitError('\n'.join(errors))]
510
511 return []
512
513
Kevin Lubick2afb9c42022-11-01 14:30:10 -0400514def _CheckDEPS(input_api, output_api):
515 """If DEPS was modified, run the deps_parser to update bazel/deps.bzl"""
516 needs_running = False
517 for affected_file in input_api.AffectedFiles(include_deletes=False):
518 affected_file_path = affected_file.LocalPath()
519 if affected_file_path.endswith('DEPS') or affected_file_path.endswith('deps.bzl'):
520 needs_running = True
521 break
522 if not needs_running:
523 return []
524 try:
525 subprocess.check_output(
526 ['bazelisk', '--version'],
527 stderr=subprocess.STDOUT)
528 except:
529 return [output_api.PresubmitNotifyResult(
530 'Skipping DEPS check because bazelisk is not on PATH. \n' +
531 'You can download it from https://github.com/bazelbuild/bazelisk/releases/tag/v1.14.0')]
532
533 return _RunCommandAndCheckGitDiff(
534 output_api, ['bazelisk', 'run', '//bazel/deps_parser'])
535
536
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000537def _CommonChecks(input_api, output_api):
538 """Presubmit checks common to upload and commit."""
539 results = []
540 sources = lambda x: (x.LocalPath().endswith('.h') or
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000541 x.LocalPath().endswith('.py') or
542 x.LocalPath().endswith('.sh') or
mtklein18e55802015-03-25 07:21:20 -0700543 x.LocalPath().endswith('.m') or
544 x.LocalPath().endswith('.mm') or
545 x.LocalPath().endswith('.go') or
546 x.LocalPath().endswith('.c') or
547 x.LocalPath().endswith('.cc') or
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000548 x.LocalPath().endswith('.cpp'))
Ben Wagner88855502017-10-12 17:55:19 -0400549 results.extend(_CheckChangeHasEol(
550 input_api, output_api, source_file_filter=sources))
551 with _WarningsAsErrors(output_api):
552 results.extend(input_api.canned_checks.CheckChangeHasNoCR(
553 input_api, output_api, source_file_filter=sources))
554 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
555 input_api, output_api, source_file_filter=sources))
Ben Wagnercf42e982018-02-09 17:41:20 -0500556 results.extend(_JsonChecks(input_api, output_api))
rmistry01cbf6c2015-03-12 07:48:40 -0700557 results.extend(_IfDefChecks(input_api, output_api))
borenetc7c91802015-03-25 04:47:02 -0700558 results.extend(_CopyrightChecks(input_api, output_api,
559 source_file_filter=sources))
Mike Kleinbb413432019-07-26 11:55:40 -0500560 results.extend(_CheckIncludesFormatted(input_api, output_api))
Mike Klein96f64012020-04-03 10:59:37 -0500561 results.extend(_CheckGNFormatted(input_api, output_api))
Ravi Mistry6eca5792020-12-16 11:42:29 -0500562 results.extend(_CheckGitConflictMarkers(input_api, output_api))
Kevin Lubick2cd80672021-07-01 11:03:36 -0400563 results.extend(_RegenerateAllExamplesCPP(input_api, output_api))
Kevin Lubickd7dc6d72023-02-13 09:28:29 -0500564 results.extend(_CheckExamplesForPrivateAPIs(input_api, output_api))
Kevin Lubickad09d4b2022-05-04 09:59:13 -0400565 results.extend(_CheckBazelBUILDFiles(input_api, output_api))
Kevin Lubick07e7bc12022-10-03 09:18:35 -0400566 results.extend(_CheckBannedAPIs(input_api, output_api))
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000567 return results
568
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000569
570def CheckChangeOnUpload(input_api, output_api):
Ravi Mistry4c0ffe72020-03-02 13:19:02 -0500571 """Presubmit checks for the change on upload."""
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000572 results = []
573 results.extend(_CommonChecks(input_api, output_api))
borenet1ed2ae42016-07-26 11:52:17 -0700574 # Run on upload, not commit, since the presubmit bot apparently doesn't have
borenet60b0a2d2016-10-04 12:45:41 -0700575 # coverage or Go installed.
borenet2dbbfa52016-10-14 06:32:09 -0700576 results.extend(_InfraTests(input_api, output_api))
Chris Mumfordbe879e02023-04-24 09:07:20 -0700577 results.extend(_CheckTopReleaseNotesChanged(input_api, output_api))
Ravi Mistry57735162019-07-25 13:45:15 -0400578 results.extend(_CheckReleaseNotesForPublicAPI(input_api, output_api))
Kevin Lubick8d9d9fa2022-05-17 13:43:52 -0400579 # Only check public.bzl on upload because new files are likely to be a source
580 # of false positives and we don't want to unnecessarily block commits.
581 results.extend(_CheckPublicBzl(input_api, output_api))
Kevin Lubick2c655792022-05-27 13:56:03 -0400582 # Buildifier might not be on the CI machines.
583 results.extend(_CheckBuildifier(input_api, output_api))
Kevin Lubick2afb9c42022-11-01 14:30:10 -0400584 # We don't want this to block the CQ (for now).
585 results.extend(_CheckDEPS(input_api, output_api))
Kevin Lubickdd0bfa02022-12-02 08:37:58 -0500586 # Bazelisk is not yet included in the Presubmit job.
587 results.extend(_CheckGeneratedBazelBUILDFiles(input_api, output_api))
Chris Mumford008981f2022-12-01 08:29:24 -0800588 results.extend(_CheckGNIGenerated(input_api, output_api))
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000589 return results
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000590
591
rmistryb398ecc2016-08-29 08:13:29 -0700592class CodeReview(object):
593 """Abstracts which codereview tool is used for the specified issue."""
594
595 def __init__(self, input_api):
596 self._issue = input_api.change.issue
597 self._gerrit = input_api.gerrit
rmistryb398ecc2016-08-29 08:13:29 -0700598
599 def GetOwnerEmail(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700600 return self._gerrit.GetChangeOwner(self._issue)
rmistryb398ecc2016-08-29 08:13:29 -0700601
602 def GetSubject(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700603 return self._gerrit.GetChangeInfo(self._issue)['subject']
rmistryb398ecc2016-08-29 08:13:29 -0700604
605 def GetDescription(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700606 return self._gerrit.GetChangeDescription(self._issue)
rmistryb398ecc2016-08-29 08:13:29 -0700607
Ravi Mistry39eabb62016-10-05 08:41:12 -0400608 def GetReviewers(self):
Aaron Gablea49909a2017-10-09 12:50:52 -0700609 code_review_label = (
610 self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review'])
611 return [r['email'] for r in code_review_label.get('all', [])]
Ravi Mistry39eabb62016-10-05 08:41:12 -0400612
rmistryb398ecc2016-08-29 08:13:29 -0700613 def GetApprovers(self):
614 approvers = []
Aaron Gablea49909a2017-10-09 12:50:52 -0700615 code_review_label = (
616 self._gerrit.GetChangeInfo(self._issue)['labels']['Code-Review'])
617 for m in code_review_label.get('all', []):
618 if m.get("value") == 1:
619 approvers.append(m["email"])
rmistryb398ecc2016-08-29 08:13:29 -0700620 return approvers
621
622
Ravi Mistry57735162019-07-25 13:45:15 -0400623def _CheckReleaseNotesForPublicAPI(input_api, output_api):
Chris Mumfordbe879e02023-04-24 09:07:20 -0700624 """Checks to see if a release notes file is added or edited with public API changes."""
Ravi Mistry57735162019-07-25 13:45:15 -0400625 results = []
626 public_api_changed = False
627 release_file_changed = False
628 for affected_file in input_api.AffectedFiles():
629 affected_file_path = affected_file.LocalPath()
630 file_path, file_ext = os.path.splitext(affected_file_path)
631 # We only care about files that end in .h and are under the top-level
632 # include dir, but not include/private.
633 if (file_ext == '.h' and
Chris Mumford0e8fd7b2023-04-24 14:04:54 -0700634 file_path.split(os.path.sep)[0] == 'include' and
Ravi Mistry57735162019-07-25 13:45:15 -0400635 'private' not in file_path):
636 public_api_changed = True
Chris Mumfordbe879e02023-04-24 09:07:20 -0700637 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR:
Ravi Mistry57735162019-07-25 13:45:15 -0400638 release_file_changed = True
639
640 if public_api_changed and not release_file_changed:
641 results.append(output_api.PresubmitPromptWarning(
Chris Mumfordbe879e02023-04-24 09:07:20 -0700642 'If this change affects a client API, please add a new summary '
643 'file in the %s directory. More information can be found in '
644 '%s.' % (RELEASE_NOTES_DIR, RELEASE_NOTES_README)))
645 return results
646
647
648def _CheckTopReleaseNotesChanged(input_api, output_api):
649 """Warns if the top level release notes file was changed.
650
651 The top level file is now auto-edited, and new release notes should
652 be added to the RELEASE_NOTES_DIR directory"""
653 results = []
654 top_relnotes_changed = False
655 release_file_changed = False
656 for affected_file in input_api.AffectedFiles():
657 affected_file_path = affected_file.LocalPath()
658 file_path, file_ext = os.path.splitext(affected_file_path)
659 if affected_file_path == RELEASE_NOTES_FILE_NAME:
660 top_relnotes_changed = True
661 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR:
662 release_file_changed = True
663 # When relnotes_util is run it will modify RELEASE_NOTES_FILE_NAME
664 # and delete the individual note files in RELEASE_NOTES_DIR.
665 # So, if both paths are modified do not emit a warning.
666 if top_relnotes_changed and not release_file_changed:
667 results.append(output_api.PresubmitPromptWarning(
668 'Do not edit %s directly. %s is automatically edited during the '
669 'release process. Release notes should be added as new files in '
670 'the %s directory. More information can be found in %s.' % (RELEASE_NOTES_FILE_NAME,
671 RELEASE_NOTES_FILE_NAME,
672 RELEASE_NOTES_DIR,
673 RELEASE_NOTES_README)))
Ravi Mistry57735162019-07-25 13:45:15 -0400674 return results
675
676
Edward Lemur2b7876c2020-01-17 18:48:13 -0500677def PostUploadHook(gerrit, change, output_api):
rmistryd223fb22015-02-26 10:16:13 -0800678 """git cl upload will call this hook after the issue is created/modified.
679
680 This hook does the following:
681 * Adds a link to preview docs changes if there are any docs changes in the CL.
Ravi Mistry355feab2017-05-23 14:24:08 -0400682 * Adds 'No-Try: true' if the CL contains only docs changes.
rmistryd223fb22015-02-26 10:16:13 -0800683 """
Edward Lemur2b7876c2020-01-17 18:48:13 -0500684 if not change.issue:
685 return []
686
687 # Skip PostUploadHooks for all auto-commit service account bots. New
688 # patchsets (caused due to PostUploadHooks) invalidates the CQ+2 vote from
689 # the "--use-commit-queue" flag to "git cl upload".
690 for suffix in SERVICE_ACCOUNT_SUFFIX:
691 if change.author_email.endswith(suffix):
692 return []
rmistryd223fb22015-02-26 10:16:13 -0800693
694 results = []
Ravi Mistry27095f22021-04-22 12:51:49 +0000695 at_least_one_docs_change = False
rmistryd223fb22015-02-26 10:16:13 -0800696 all_docs_changes = True
697 for affected_file in change.AffectedFiles():
698 affected_file_path = affected_file.LocalPath()
699 file_path, _ = os.path.splitext(affected_file_path)
Ravi Mistry27095f22021-04-22 12:51:49 +0000700 if 'site' == file_path.split(os.path.sep)[0]:
701 at_least_one_docs_change = True
rmistryd223fb22015-02-26 10:16:13 -0800702 else:
703 all_docs_changes = False
Ravi Mistry27095f22021-04-22 12:51:49 +0000704 if at_least_one_docs_change and not all_docs_changes:
705 break
rmistryd223fb22015-02-26 10:16:13 -0800706
Edward Lemur2b7876c2020-01-17 18:48:13 -0500707 footers = change.GitFootersFromDescription()
708 description_changed = False
Ravi Mistryb5e2acc2017-12-07 11:10:11 -0500709
Edward Lemur2b7876c2020-01-17 18:48:13 -0500710 # If the change includes only doc changes then add No-Try: true in the
711 # CL's description if it does not exist yet.
712 if all_docs_changes and 'true' not in footers.get('No-Try', []):
713 description_changed = True
Edward Lemurc631b7c2020-02-04 15:30:18 -0500714 change.AddDescriptionFooter('No-Try', 'true')
Edward Lemur2b7876c2020-01-17 18:48:13 -0500715 results.append(
716 output_api.PresubmitNotifyResult(
717 'This change has only doc changes. Automatically added '
718 '\'No-Try: true\' to the CL\'s description'))
rmistryd223fb22015-02-26 10:16:13 -0800719
Edward Lemur2b7876c2020-01-17 18:48:13 -0500720 # If the description has changed update it.
721 if description_changed:
722 gerrit.UpdateDescription(
723 change.FullDescriptionText(), change.issue)
rmistryd223fb22015-02-26 10:16:13 -0800724
Edward Lemur2b7876c2020-01-17 18:48:13 -0500725 return results
rmistryd223fb22015-02-26 10:16:13 -0800726
727
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000728def CheckChangeOnCommit(input_api, output_api):
Ravi Mistry4c0ffe72020-03-02 13:19:02 -0500729 """Presubmit checks for the change on commit."""
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000730 results = []
rmistry@google.com6be0b4c2013-01-17 14:50:59 +0000731 results.extend(_CommonChecks(input_api, output_api))
Ravi Mistrya70cb8a2017-09-12 13:52:05 -0400732 # Checks for the presence of 'DO NOT''SUBMIT' in CL description and in
733 # content of files.
734 results.extend(
735 input_api.canned_checks.CheckDoNotSubmit(input_api, output_api))
rmistry@google.com8e3ff8c2013-01-17 12:55:34 +0000736 return results