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']]
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:',
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)))
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)))
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))
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)))
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)))
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)))
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):
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.' % (
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))]
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')):
283 [
'make',
'-C',
'bazel',
'generate_go'])
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
331 """Reminds devs to add/remove files from public.bzl."""
334 with open(
'public.bzl',
'r', encoding=
'utf-8')
as f:
335 public_bzl = f.read().strip()
336 for affected_file
in input_api.AffectedFiles(include_deletes=
True):
338 action = affected_file.Action()
339 affected_file_path = affected_file.LocalPath()
340 if ((affected_file_path.startswith(
"include")
or affected_file_path.startswith(
"src"))
and
341 (affected_file_path.endswith(
".cpp")
or affected_file_path.endswith(
".h")
or
342 affected_file_path.endswith(
".mm"))):
343 affected_file_path =
'"' + affected_file_path +
'"'
344 if action ==
"D" and affected_file_path
in public_bzl:
345 results.append(output_api.PresubmitError(
346 "Need to delete %s from public.bzl (or rename it)" % affected_file_path))
347 elif action ==
"A" and affected_file_path
not in public_bzl:
348 results.append(output_api.PresubmitPromptWarning(
349 "You may need to add %s to public.bzl" % affected_file_path))
354 """Run an arbitrary command. Fail if it produces any diffs."""
355 command_str =
' '.join(command)
359 output = subprocess.check_output(
361 stderr=subprocess.STDOUT, encoding=
'utf-8')
362 except subprocess.CalledProcessError
as e:
363 results += [output_api.PresubmitError(
364 'Command "%s" returned non-zero exit code %d. Output: \n\n%s' % (
371 git_diff_output = subprocess.check_output(
372 [
'git',
'diff',
'--no-ext-diff'], encoding=
'utf-8')
374 results += [output_api.PresubmitError(
375 'Diffs found after running "%s":\n\n%s\n'
376 'Please commit or discard the above changes.' % (
386 """Ensures that the generated *.gni files are current.
388 The Bazel project files are authoritative and some *.gni files are
389 generated from them using the exporter_tool. This check ensures they
392 if 'win32' in sys.platform:
396 output_api.PresubmitPromptWarning(
397 'Skipping Bazel=>GNI export check on Windows (unsupported platform).'
400 if 'darwin' in sys.platform:
404 for affected_file
in input_api.AffectedFiles(include_deletes=
True):
405 affected_file_path = affected_file.LocalPath()
406 if affected_file_path.endswith(
'BUILD.bazel')
or affected_file_path.endswith(
'.gni'):
411 [
'make',
'-C',
'bazel',
'generate_gni'])
418 """Runs Buildifier and fails on linting errors, or if it produces any diffs.
420 This check only runs if the affected files include any WORKSPACE, BUILD,
421 BUILD.bazel or *.bzl files.
425 for affected_file
in input_api.AffectedFiles(include_deletes=
False):
426 affected_file_path = affected_file.LocalPath()
427 if affected_file_path.endswith(
'BUILD.bazel')
or affected_file_path.endswith(
'.bzl'):
428 if not affected_file_path.endswith(
'public.bzl')
and \
429 not affected_file_path.endswith(
'go_repositories.bzl')
and \
430 not "bazel/rbe/gce_linux/" in affected_file_path
and \
431 not affected_file_path.startswith(
"third_party/externals/")
and \
432 not "node_modules/" in affected_file_path:
433 files.append(affected_file_path)
437 subprocess.check_output(
438 [
'buildifier',
'--version'],
439 stderr=subprocess.STDOUT)
441 return [output_api.PresubmitNotifyResult(
442 'Skipping buildifier check because it is not on PATH. \n' +
443 'You can download it from https://github.com/bazelbuild/buildtools/releases')]
465 """Check source code for functions and packages that should not be used."""
470 banned_replacements = [
471 (
r'std::stof\(',
'std::strtof(), which does not throw'),
472 (
r'std::stod\(',
'std::strtod(), which does not throw'),
473 (
r'std::stold\(',
'std::strtold(), which does not throw'),
478 existence_defines = [
'SK_GANESH',
'SK_GRAPHITE',
'SK_GL',
'SK_VULKAN',
'SK_DAWN',
'SK_METAL',
479 'SK_DIRECT3D',
'SK_DEBUG',
'GR_TEST_UTILS',
'GRAPHITE_TEST_UTILS']
480 for d
in existence_defines:
481 banned_replacements.append((
'#if {}'.
format(d),
482 '#if defined({})'.
format(d)))
483 compiled_replacements = []
484 for rep
in banned_replacements:
487 (re, replacement, exceptions) = rep
489 (re, replacement) = rep
491 compiled_re = input_api.re.compile(re)
492 compiled_exceptions = [input_api.re.compile(exc)
for exc
in exceptions]
493 compiled_replacements.append(
494 (compiled_re, replacement, compiled_exceptions))
497 file_filter =
lambda x: (x.LocalPath().endswith(
'.h')
or
498 x.LocalPath().endswith(
'.cpp')
or
499 x.LocalPath().endswith(
'.cc')
or
500 x.LocalPath().endswith(
'.m')
or
501 x.LocalPath().endswith(
'.mm'))
502 for affected_file
in input_api.AffectedSourceFiles(file_filter):
503 affected_filepath = affected_file.LocalPath()
504 for (line_num, line)
in affected_file.ChangedContents():
505 for (re, replacement, exceptions)
in compiled_replacements:
506 match = re.search(line)
508 for exc
in exceptions:
509 if exc.search(affected_filepath):
512 errors.append(
'%s:%s: Instead of %s, please use %s.' % (
513 affected_filepath, line_num, match.group(), replacement))
516 return [output_api.PresubmitError(
'\n'.join(errors))]
522 """If DEPS was modified, run the deps_parser to update bazel/deps.bzl"""
523 needs_running =
False
524 for affected_file
in input_api.AffectedFiles(include_deletes=
False):
525 affected_file_path = affected_file.LocalPath()
526 if affected_file_path.endswith(
'DEPS')
or affected_file_path.endswith(
'deps.bzl'):
529 if not needs_running:
532 subprocess.check_output(
533 [
'bazelisk',
'--version'],
534 stderr=subprocess.STDOUT)
536 return [output_api.PresubmitNotifyResult(
537 'Skipping DEPS check because bazelisk is not on PATH. \n' +
538 'You can download it from https://github.com/bazelbuild/bazelisk/releases/tag/v1.14.0')]
541 output_api, [
'bazelisk',
'run',
'//bazel/deps_parser'])
545 """Presubmit checks common to upload and commit."""
547 sources =
lambda x: (x.LocalPath().endswith(
'.h')
or
548 x.LocalPath().endswith(
'.py')
or
549 x.LocalPath().endswith(
'.sh')
or
550 x.LocalPath().endswith(
'.m')
or
551 x.LocalPath().endswith(
'.mm')
or
552 x.LocalPath().endswith(
'.go')
or
553 x.LocalPath().endswith(
'.c')
or
554 x.LocalPath().endswith(
'.cc')
or
555 x.LocalPath().endswith(
'.cpp'))
557 input_api, output_api, source_file_filter=sources))
559 results.extend(input_api.canned_checks.CheckChangeHasNoCR(
560 input_api, output_api, source_file_filter=sources))
561 results.extend(input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
562 input_api, output_api, source_file_filter=sources))
566 source_file_filter=sources))
578 """Presubmit checks for the change on upload."""
592 results.extend(
_CheckDEPS(input_api, output_api))
600 """Abstracts which codereview tool is used for the specified issue."""
616 code_review_label = (
617 self.
_gerrit.GetChangeInfo(self.
_issue)[
'labels'][
'Code-Review'])
618 return [r[
'email']
for r
in code_review_label.get(
'all', [])]
622 code_review_label = (
623 self.
_gerrit.GetChangeInfo(self.
_issue)[
'labels'][
'Code-Review'])
624 for m
in code_review_label.get(
'all', []):
625 if m.get(
"value") == 1:
626 approvers.append(m[
"email"])
631 """Checks to see if a release notes file is added or edited with public API changes."""
633 public_api_changed =
False
634 release_file_changed =
False
635 for affected_file
in input_api.AffectedFiles():
636 affected_file_path = affected_file.LocalPath()
637 file_path, file_ext = os.path.splitext(affected_file_path)
640 if (file_ext ==
'.h' and
641 file_path.split(os.path.sep)[0] ==
'include' and
642 'private' not in file_path):
643 public_api_changed =
True
644 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR:
645 release_file_changed =
True
647 if public_api_changed
and not release_file_changed:
648 results.append(output_api.PresubmitPromptWarning(
649 'If this change affects a client API, please add a new summary '
650 'file in the %s directory. More information can be found in '
651 '%s.' % (RELEASE_NOTES_DIR, RELEASE_NOTES_README)))
656 """Warns if the top level release notes file was changed.
658 The top level file is now auto-edited, and new release notes should
659 be added to the RELEASE_NOTES_DIR directory"""
661 top_relnotes_changed =
False
662 release_file_changed =
False
663 for affected_file
in input_api.AffectedFiles():
664 affected_file_path = affected_file.LocalPath()
665 file_path, file_ext = os.path.splitext(affected_file_path)
666 if affected_file_path == RELEASE_NOTES_FILE_NAME:
667 top_relnotes_changed =
True
668 elif os.path.dirname(file_path) == RELEASE_NOTES_DIR:
669 release_file_changed =
True
673 if top_relnotes_changed
and not release_file_changed:
674 results.append(output_api.PresubmitPromptWarning(
675 'Do not edit %s directly. %s is automatically edited during the '
676 'release process. Release notes should be added as new files in '
677 'the %s directory. More information can be found in %s.' % (RELEASE_NOTES_FILE_NAME,
678 RELEASE_NOTES_FILE_NAME,
680 RELEASE_NOTES_README)))
685 """git cl upload will call this hook after the issue is created/modified.
687 This hook does the following:
688 * Adds a link to preview docs changes if there are any docs changes in the CL.
689 * Adds 'No-Try: true' if the CL contains only docs changes.
697 for suffix
in SERVICE_ACCOUNT_SUFFIX:
698 if change.author_email.endswith(suffix):
702 at_least_one_docs_change =
False
703 all_docs_changes =
True
704 for affected_file
in change.AffectedFiles():
705 affected_file_path = affected_file.LocalPath()
706 file_path, _ = os.path.splitext(affected_file_path)
707 if 'site' == file_path.split(os.path.sep)[0]:
708 at_least_one_docs_change =
True
710 all_docs_changes =
False
711 if at_least_one_docs_change
and not all_docs_changes:
714 footers = change.GitFootersFromDescription()
715 description_changed =
False
719 if all_docs_changes
and 'true' not in footers.get(
'No-Try', []):
720 description_changed =
True
721 change.AddDescriptionFooter(
'No-Try',
'true')
723 output_api.PresubmitNotifyResult(
724 'This change has only doc changes. Automatically added '
725 '\'No-Try: true\' to the CL\'s description'))
728 if description_changed:
729 gerrit.UpdateDescription(
730 change.FullDescriptionText(), change.issue)
736 """Presubmit checks for the change on commit."""
742 input_api.canned_checks.CheckDoNotSubmit(input_api, output_api))
__init__(self, input_api)
__exit__(self, ex_type, ex_value, ex_traceback)
__init__(self, output_api)
uint32_t uint32_t * format
_InfraTests(input_api, output_api)
CheckChangeOnUpload(input_api, output_api)
_CheckIncludesFormatted(input_api, output_api)
_CheckGitConflictMarkers(input_api, output_api)
_RegenerateAllExamplesCPP(input_api, output_api)
_CheckDEPS(input_api, output_api)
_CheckBuildifier(input_api, output_api)
_CheckReleaseNotesForPublicAPI(input_api, output_api)
_CheckGNIGenerated(input_api, output_api)
_CheckTopReleaseNotesChanged(input_api, output_api)
PostUploadHook(gerrit, change, output_api)
_IfDefChecks(input_api, output_api)
_CommonChecks(input_api, output_api)
_JsonChecks(input_api, output_api)
CheckChangeOnCommit(input_api, output_api)
_CopyrightChecks(input_api, output_api, source_file_filter=None)
_CheckGNFormatted(input_api, output_api)
_CheckBannedAPIs(input_api, output_api)
_CheckChangeHasEol(input_api, output_api, source_file_filter=None)
_RunCommandAndCheckGitDiff(output_api, command)
_CheckExamplesForPrivateAPIs(input_api, output_api)
_CheckBazelBUILDFiles(input_api, output_api)
_CheckPublicBzl(input_api, output_api)
_CheckGeneratedBazelBUILDFiles(input_api, output_api)