7"""Top-level presubmit script for Skia.
9See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
10for more details about the presubmit API built into gcl.
21RELEASE_NOTES_DIR =
'relnotes'
22RELEASE_NOTES_FILE_NAME =
'RELEASE_NOTES.md'
23RELEASE_NOTES_README =
'//relnotes/README.md'
25GOLD_TRYBOT_URL =
'https://gold.skia.org/search?issue='
27SERVICE_ACCOUNT_SUFFIX = [
28 '@%s.iam.gserviceaccount.com' % project
for project
in [
29 'skia-buildbots.google.com',
'skia-swarming-bots',
'skia-public',
30 'skia-corp.google.com',
'chops-service-accounts']]
35def _CheckChangeHasEol(input_api, output_api, source_file_filter=None):
36 """Checks that files end with at least one \n (LF)."""
38 for f
in input_api.AffectedSourceFiles(source_file_filter):
39 contents = input_api.ReadFile(f,
'rb')
41 if len(contents) > 1
and contents[-1:] !=
'\n':
42 eof_files.append(f.LocalPath())
45 return [output_api.PresubmitPromptWarning(
46 'These files should end in a newline character:',
51def _JsonChecks(input_api, output_api):
52 """Run checks on any modified json 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:
61 input_api.json.load(open(affected_file_path,
'r'))
63 failing_files.append(affected_file_path)
68 output_api.PresubmitError(
69 'The following files contain invalid json:\n%s\n\n' %
70 '\n'.
join(failing_files)))
74def _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*//.*$')
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))
86 empty_line_pattern = re.compile(
'^\s*$')
87 def is_empty_line(line):
88 return empty_line_pattern.match(line)
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)
96 if is_comment(line)
or is_empty_line(line):
99 if line.startswith(
'#if 0 '):
101 elif line.startswith(
'#if ')
or line.startswith(
'#ifdef '):
102 failing_files.append(affected_file_path)
108 output_api.PresubmitError(
109 'The following files have #if or #ifdef before includes:\n%s\n\n'
110 'See https://bug.skia.org/3362 for why this should be fixed.' %
111 '\n'.
join(failing_files)))
115def _CopyrightChecks(input_api, output_api, source_file_filter=None):
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)
123 for affected_file
in input_api.AffectedSourceFiles(source_file_filter):
124 if (
'third_party/' in affected_file.LocalPath()
or
125 'tests/sksl/' in affected_file.LocalPath()
or
126 'bazel/rbe/' in affected_file.LocalPath()
or
127 'bazel/external/' in affected_file.LocalPath()
or
128 'bazel/exporter/interfaces/mocks/' in affected_file.LocalPath()):
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))
137def _InfraTests(input_api, output_api):
138 """Run the infra tests."""
140 if not any(f.LocalPath().startswith(
'infra')
141 for f
in input_api.AffectedFiles()):
144 cmd = [
'python3', os.path.join(
'infra',
'bots',
'infra_tests.py')]
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)))
153def _CheckGNFormatted(input_api, output_api):
154 """Make sure any .gn files we're changing have been formatted."""
156 for f
in input_api.AffectedFiles(include_deletes=
False):
157 if (f.LocalPath().endswith(
'.gn')
or
158 f.LocalPath().endswith(
'.gni')):
163 cmd = [
'python3', os.path.join(
'bin',
'fetch-gn')]
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))]
172 gn =
'gn.exe' if 'win32' in sys.platform
else 'gn'
173 gn = os.path.join(input_api.PresubmitLocalPath(),
'bin', gn)
174 cmd = [gn,
'format',
'--dry-run', f.LocalPath()]
176 subprocess.check_output(cmd)
177 except subprocess.CalledProcessError:
178 fix =
'bin/gn format ' + f.LocalPath()
179 results.append(output_api.PresubmitError(
180 '`%s` failed, try\n\t%s' % (
' '.
join(cmd), fix)))
184def _CheckGitConflictMarkers(input_api, output_api):
185 pattern = input_api.re.compile(
'^(?:<<<<<<<|>>>>>>>) |^=======$')
187 for f
in input_api.AffectedFiles():
188 for line_num, line
in f.ChangedContents():
189 if f.LocalPath().endswith(
'.md'):
193 if pattern.match(line):
195 output_api.PresubmitError(
196 'Git conflict markers found in %s:%d %s' % (
197 f.LocalPath(), line_num, line)))
201def _CheckIncludesFormatted(input_api, output_api):
202 """Make sure #includes in files we're changing have been formatted."""
203 files = [str(f)
for f
in input_api.AffectedFiles()
if f.Action() !=
'D']
205 'tools/rewrite_includes.py',
207 if 0 != subprocess.call(cmd):
208 return [output_api.PresubmitError(
'`%s` failed' %
' '.
join(cmd))]
220 def __exit__(self, ex_type, ex_value, ex_traceback):
224def _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()):
229 command_str =
'tools/fiddle/make_all_examples_cpp.py'
230 cmd = [
'python3', command_str]
231 if 0 != subprocess.call(cmd):
232 return [output_api.PresubmitError(
'`%s` failed' %
' '.
join(cmd))]
235 git_diff_output = input_api.subprocess.check_output(
236 [
'git',
'diff',
'--no-ext-diff'])
238 results += [output_api.PresubmitError(
239 'Diffs found after running "%s":\n\n%s\n'
240 'Please commit or discard the above changes.' % (
248def _CheckExamplesForPrivateAPIs(input_api, output_api):
249 """We only want our checked-in examples (aka fiddles) to show public API."""
251 input_api.re.compile(
r'#\s*include\s+("src/.*)'),
252 input_api.re.compile(
r'#\s*include\s+("include/private/.*)'),
254 file_filter =
lambda x: (x.LocalPath().startswith(
'docs/examples/'))
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)
262 errors.append(
'%s:%s: Fiddles should not use private/internal API like %s.' % (
263 affected_filepath, line_num, match.group(1)))
266 return [output_api.PresubmitError(
'\n'.
join(errors))]
270def _CheckGeneratedBazelBUILDFiles(input_api, output_api):
271 if 'win32' in sys.platform:
275 if 'darwin' in sys.platform:
278 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'])
287def _CheckBazelBUILDFiles(input_api, output_api):
288 """Makes sure our BUILD.bazel files are compatible with G3."""
290 for affected_file
in input_api.AffectedFiles(include_deletes=
False):
291 affected_file_path = affected_file.LocalPath()
292 is_bazel = affected_file_path.endswith(
'BUILD.bazel')
294 excluded_paths = [
"infra/",
"bazel/rbe/",
"bazel/external/",
"bazel/common_config_settings/",
295 "modules/canvaskit/go/",
"experimental/",
"bazel/platform",
"third_party/",
296 "tests/",
"resources/",
"bazel/deps_parser/",
"bazel/exporter_tool/",
297 "tools/gpu/gl/interface/",
"bazel/utils/",
"include/config/",
298 "bench/",
"example/external_client/"]
299 is_excluded =
any(affected_file_path.startswith(n)
for n
in excluded_paths)
300 if is_bazel
and not is_excluded:
301 with open(affected_file_path,
'r')
as file:
302 contents = file.read()
303 if 'exports_files_legacy(' not in contents:
304 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
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
314 if 'cc_library(' in contents
and '"skia_cc_library"' not in contents:
315 results.append(output_api.PresubmitError(
316 (
'%s needs to load skia_cc_library from macros.bzl instead of using the ' +
317 'native one. This allows us to build differently for G3.\n' +
318 'Add "skia_cc_library" to load("//bazel:macros.bzl", ...)')
321 if 'default_applicable_licenses' not in contents:
323 results.append(output_api.PresubmitError(
324 (
'%s needs to have\npackage(default_applicable_licenses = ["//:license"])\n'+
325 'to comply with G3 policies') % affected_file_path
330def _RunCommandAndCheckGitDiff(output_api, command):
331 """Run an arbitrary command. Fail if it produces any diffs."""
332 command_str =
' '.
join(command)
336 output = subprocess.check_output(
338 stderr=subprocess.STDOUT, encoding=
'utf-8')
339 except subprocess.CalledProcessError
as e:
340 results += [output_api.PresubmitError(
341 'Command "%s" returned non-zero exit code %d. Output: \n\n%s' % (
348 git_diff_output = subprocess.check_output(
349 [
'git',
'diff',
'--no-ext-diff'], encoding=
'utf-8')
351 results += [output_api.PresubmitError(
352 'Diffs found after running "%s":\n\n%s\n'
353 'Please commit or discard the above changes.' % (
362def _CheckGNIGenerated(input_api, output_api):
363 """Ensures that the generated *.gni files are current.
365 The Bazel project files are authoritative and some *.gni files are
366 generated
from them using the exporter_tool. This check ensures they
369 if 'win32' in sys.platform:
373 output_api.PresubmitPromptWarning(
374 'Skipping Bazel=>GNI export check on Windows (unsupported platform).'
377 if 'darwin' in sys.platform:
381 for affected_file
in input_api.AffectedFiles(include_deletes=
True):
382 affected_file_path = affected_file.LocalPath()
383 if affected_file_path.endswith(
'BUILD.bazel')
or affected_file_path.endswith(
'.gni'):
387 return _RunCommandAndCheckGitDiff(output_api,
388 [
'make',
'-C',
'bazel',
'generate_gni'])
394def _CheckBuildifier(input_api, output_api):
395 """Runs Buildifier and fails on linting errors, or if it produces any diffs.
397 This check only runs if the affected files include any WORKSPACE, BUILD,
398 BUILD.bazel
or *.bzl files.
402 for affected_file
in input_api.AffectedFiles(include_deletes=
False):
403 affected_file_path = affected_file.LocalPath()
404 if affected_file_path.endswith(
'BUILD.bazel')
or affected_file_path.endswith(
'.bzl'):
405 if not affected_file_path.endswith(
'public.bzl')
and \
406 not affected_file_path.endswith(
'go_repositories.bzl')
and \
407 not "bazel/rbe/gce_linux/" in affected_file_path
and \
408 not affected_file_path.startswith(
"third_party/externals/")
and \
409 not "node_modules/" in affected_file_path:
410 files.append(affected_file_path)
414 subprocess.check_output(
415 [
'buildifier',
'--version'],
416 stderr=subprocess.STDOUT)
418 return [output_api.PresubmitNotifyResult(
419 'Skipping buildifier check because it is not on PATH. \n' +
420 'You can download it from https://github.com/bazelbuild/buildtools/releases')]
422 return _RunCommandAndCheckGitDiff(
441def _CheckBannedAPIs(input_api, output_api):
442 """Check source code for functions and packages that should not be used."""
447 banned_replacements = [
448 (
r'std::stof\(',
'std::strtof(), which does not throw'),
449 (
r'std::stod\(',
'std::strtod(), which does not throw'),
450 (
r'std::stold\(',
'std::strtold(), which does not throw'),
455 existence_defines = [
'SK_GANESH',
'SK_GRAPHITE',
'SK_GL',
'SK_VULKAN',
'SK_DAWN',
'SK_METAL',
456 'SK_DIRECT3D',
'SK_DEBUG',
'GR_TEST_UTILS',
'GRAPHITE_TEST_UTILS']
457 for d
in existence_defines:
458 banned_replacements.append((
'#if {}'.
format(d),
459 '#if defined({})'.
format(d)))
460 compiled_replacements = []
461 for rep
in banned_replacements:
464 (re, replacement, exceptions) = rep
466 (re, replacement) = rep
468 compiled_re = input_api.re.compile(re)
469 compiled_exceptions = [input_api.re.compile(exc)
for exc
in exceptions]
470 compiled_replacements.append(
471 (compiled_re, replacement, compiled_exceptions))
474 file_filter =
lambda x: (x.LocalPath().endswith(
'.h')
or
475 x.LocalPath().endswith(
'.cpp')
or
476 x.LocalPath().endswith(
'.cc')
or
477 x.LocalPath().endswith(
'.m')
or
478 x.LocalPath().endswith(
'.mm'))
479 for affected_file
in input_api.AffectedSourceFiles(file_filter):
480 affected_filepath = affected_file.LocalPath()
481 for (line_num, line)
in affected_file.ChangedContents():
482 for (re, replacement, exceptions)
in compiled_replacements:
483 match = re.search(line)
485 for exc
in exceptions:
486 if exc.search(affected_filepath):
489 errors.append(
'%s:%s: Instead of %s, please use %s.' % (
490 affected_filepath, line_num, match.group(), replacement))
493 return [output_api.PresubmitError(
'\n'.
join(errors))]
498def _CheckDEPS(input_api, output_api):
499 """If DEPS was modified, run the deps_parser to update bazel/deps.bzl"""
500 needs_running =
False
501 for affected_file
in input_api.AffectedFiles(include_deletes=
False):
502 affected_file_path = affected_file.LocalPath()
503 if affected_file_path.endswith(
'DEPS')
or affected_file_path.endswith(
'deps.bzl'):
506 if not needs_running:
509 subprocess.check_output(
510 [
'bazelisk',
'--version'],
511 stderr=subprocess.STDOUT)
513 return [output_api.PresubmitNotifyResult(
514 'Skipping DEPS check because bazelisk is not on PATH. \n' +
515 'You can download it from https://github.com/bazelbuild/bazelisk/releases/tag/v1.14.0')]
517 return _RunCommandAndCheckGitDiff(
518 output_api, [
'bazelisk',
'run',
'//bazel/deps_parser'])
521def _CommonChecks(input_api, output_api):
522 """Presubmit checks common to upload and commit."""
524 sources =
lambda x: (x.LocalPath().endswith(
'.h')
or
525 x.LocalPath().endswith(
'.py')
or
526 x.LocalPath().endswith(
'.sh')
or
527 x.LocalPath().endswith(
'.m')
or
528 x.LocalPath().endswith(
'.mm')
or
529 x.LocalPath().endswith(
'.go')
or
530 x.LocalPath().endswith(
'.c')
or
531 x.LocalPath().endswith(
'.cc')
or
532 x.LocalPath().endswith(
'.cpp'))
533 results.extend(_CheckChangeHasEol(
534 input_api, output_api, source_file_filter=sources))
536 results.extend(input_api.canned_checks.CheckChangeHasNoCR(
537 input_api, output_api, source_file_filter=sources))
538 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
539 input_api, output_api, source_file_filter=sources))
540 results.extend(_JsonChecks(input_api, output_api))
541 results.extend(_IfDefChecks(input_api, output_api))
542 results.extend(_CopyrightChecks(input_api, output_api,
543 source_file_filter=sources))
544 results.extend(_CheckIncludesFormatted(input_api, output_api))
545 results.extend(_CheckGNFormatted(input_api, output_api))
546 results.extend(_CheckGitConflictMarkers(input_api, output_api))
547 results.extend(_RegenerateAllExamplesCPP(input_api, output_api))
548 results.extend(_CheckExamplesForPrivateAPIs(input_api, output_api))
549 results.extend(_CheckBazelBUILDFiles(input_api, output_api))
550 results.extend(_CheckBannedAPIs(input_api, output_api))
555 """Presubmit checks for the change on upload."""
557 results.extend(_CommonChecks(input_api, output_api))
560 results.extend(_InfraTests(input_api, output_api))
561 results.extend(_CheckTopReleaseNotesChanged(input_api, output_api))
562 results.extend(_CheckReleaseNotesForPublicAPI(input_api, output_api))
564 results.extend(_CheckBuildifier(input_api, output_api))
566 results.extend(_CheckDEPS(input_api, output_api))
568 results.extend(_CheckGeneratedBazelBUILDFiles(input_api, output_api))
569 results.extend(_CheckGNIGenerated(input_api, output_api))
574 """Abstracts which codereview tool is used for the specified issue."""
577 self.
_issue = input_api.change.issue
578 self.
_gerrit = input_api.gerrit
590 code_review_label = (
591 self.
_gerrit.GetChangeInfo(self.
_issue)[
'labels'][
'Code-Review'])
592 return [r[
'email']
for r
in code_review_label.get(
'all', [])]
596 code_review_label = (
597 self.
_gerrit.GetChangeInfo(self.
_issue)[
'labels'][
'Code-Review'])
598 for m
in code_review_label.get(
'all', []):
599 if m.get(
"value") == 1:
600 approvers.append(m[
"email"])
604def _CheckReleaseNotesForPublicAPI(input_api, output_api):
605 """Checks to see if a release notes file is added or edited with public API changes."""
607 public_api_changed =
False
608 release_file_changed =
False
609 for affected_file
in input_api.AffectedFiles():
610 affected_file_path = affected_file.LocalPath()
611 file_path, file_ext = os.path.splitext(affected_file_path)
614 if (file_ext ==
'.h' and
615 file_path.split(os.path.sep)[0] ==
'include' and
616 'private' not in file_path):
617 public_api_changed =
True
618 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR:
619 release_file_changed =
True
621 if public_api_changed
and not release_file_changed:
622 results.append(output_api.PresubmitPromptWarning(
623 'If this change affects a client API, please add a new summary '
624 'file in the %s directory. More information can be found in '
625 '%s.' % (RELEASE_NOTES_DIR, RELEASE_NOTES_README)))
629def _CheckTopReleaseNotesChanged(input_api, output_api):
630 """Warns if the top level release notes file was changed.
632 The top level file is now auto-edited,
and new release notes should
633 be added to the RELEASE_NOTES_DIR directory
"""
635 top_relnotes_changed = False
636 release_file_changed =
False
637 for affected_file
in input_api.AffectedFiles():
638 affected_file_path = affected_file.LocalPath()
639 file_path, file_ext = os.path.splitext(affected_file_path)
640 if affected_file_path == RELEASE_NOTES_FILE_NAME:
641 top_relnotes_changed =
True
642 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR:
643 release_file_changed =
True
647 if top_relnotes_changed
and not release_file_changed:
648 results.append(output_api.PresubmitPromptWarning(
649 'Do not edit %s directly. %s is automatically edited during the '
650 'release process. Release notes should be added as new files in '
651 'the %s directory. More information can be found in %s.' % (RELEASE_NOTES_FILE_NAME,
652 RELEASE_NOTES_FILE_NAME,
654 RELEASE_NOTES_README)))
659 """git cl upload will call this hook after the issue is created/modified.
661 This hook does the following:
662 * Adds a link to preview docs changes if there are any docs changes
in the CL.
663 * Adds
'No-Try: true' if the CL contains only docs changes.
671 for suffix
in SERVICE_ACCOUNT_SUFFIX:
672 if change.author_email.endswith(suffix):
676 at_least_one_docs_change =
False
677 all_docs_changes =
True
678 for affected_file
in change.AffectedFiles():
679 affected_file_path = affected_file.LocalPath()
680 file_path, _ = os.path.splitext(affected_file_path)
681 if 'site' == file_path.split(os.path.sep)[0]:
682 at_least_one_docs_change =
True
684 all_docs_changes =
False
685 if at_least_one_docs_change
and not all_docs_changes:
688 footers = change.GitFootersFromDescription()
689 description_changed =
False
693 if all_docs_changes
and 'true' not in footers.get(
'No-Try', []):
694 description_changed =
True
695 change.AddDescriptionFooter(
'No-Try',
'true')
697 output_api.PresubmitNotifyResult(
698 'This change has only doc changes. Automatically added '
699 '\'No-Try: true\' to the CL\'s description'))
702 if description_changed:
703 gerrit.UpdateDescription(
704 change.FullDescriptionText(), change.issue)
710 """Presubmit checks for the change on commit."""
712 results.extend(_CommonChecks(input_api, output_api))
716 input_api.canned_checks.CheckDoNotSubmit(input_api, output_api))
def __init__(self, input_api)
def __exit__(self, ex_type, ex_value, ex_traceback)
def __init__(self, output_api)
uint32_t uint32_t * format
def CheckChangeOnCommit(input_api, output_api)
def CheckChangeOnUpload(input_api, output_api)
def PostUploadHook(gerrit, change, output_api)
SIT bool any(const Vec< 1, T > &x)
static SkString join(const CommandLineFlags::StringArray &)