presubmit.py: Add several new presubmit checks
- Add presubmit checks for:
* clang/gcc/arm GN builds
* clang-format
* clang-tidy
* yapf
* mypy
* copyright header
Many of these checks are not yet passing on the whole repo.
- Define --clean option for deleting cached .presubmiit files.
- Define --skip_init option for whether to skip the init step.
Change-Id: I223964c9fe076bfb8c882f4a358e3f8c6de6806b
diff --git a/presubmit.py b/presubmit.py
index 3ad6a1c..163450b 100755
--- a/presubmit.py
+++ b/presubmit.py
@@ -3,29 +3,35 @@
# Copyright 2019 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy
-# of the License at
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Runs the Pigweed local presubmit checks."""
+import argparse
import os
+import re
import shutil
-import subprocess
+import sys
-from pw_presubmit.presubmit_tools import *
+from pw_presubmit.presubmit_tools import call, filter_paths, PresubmitFailure
+from pw_presubmit import format_cc, presubmit_tools
def _init_cipd():
cipd = os.path.abspath('.presubmit/cipd')
call(sys.executable, 'env_setup/cipd/update.py', '--install-dir', cipd)
os.environ['PATH'] = os.pathsep.join((
- cipd, os.path.join(cipd, 'bin'), os.environ['PATH'],
+ cipd,
+ os.path.join(cipd, 'bin'),
+ os.environ['PATH'],
))
print('PATH', os.environ['PATH'])
@@ -38,7 +44,7 @@
os.environ['PATH'] = os.pathsep.join((
os.path.join(venv, 'bin'),
os.environ['PATH'],
- )) # yapf: disable
+ ))
call('python', '-m', 'pip', 'install', '--upgrade', 'pip')
call('python', '-m', 'pip', 'install',
@@ -51,18 +57,17 @@
_init_virtualenv()
-def gn_test():
- """Test with gn."""
- out = '.presubmit/gn'
- call('gn', 'gen', '--check', out)
- call('ninja', '-C', out)
+presubmit_dir = lambda *paths: os.path.join('.presubmit', *paths)
-def bazel_test():
- """Test with bazel."""
- prefix = '.presubmit/bazel-'
- call('bazel', 'build', '//...', '--symlink_prefix', prefix)
- call('bazel', 'test', '//...', '--symlink_prefix', prefix)
+#
+# GN presubmit checks
+#
+def gn_args(**kwargs):
+ return '--args=' + ' '.join(f'{arg}={val}' for arg, val in kwargs.items())
+
+
+GN_GEN = 'gn', 'gen', '--color=always', '--check'
@filter_paths(endswith=['.gn', '.gni'])
@@ -70,21 +75,236 @@
call('gn', 'format', '--dry-run', *paths)
-@filter_paths(endswith='.py')
-def pylint(paths):
- call(sys.executable, '-m', 'pylint', '-E', *paths)
+def gn_clang_build():
+ call(
+ *GN_GEN, '--export-compile-commands', presubmit_dir('clang'),
+ gn_args(pw_target_config='"//targets/host/host.gni"',
+ pw_target_toolchain='"//pw_toolchain:host_clang_os"'))
+ call('ninja', '-C', presubmit_dir('clang'))
-PRESUBMIT_PROGRAM = (
- init,
- pragma_once,
- gn_format,
- # pylint, # TODO(hepler): enable pylint when it passes
- bazel_test,
- gn_test,
+def gn_gcc_build():
+ call(
+ *GN_GEN, presubmit_dir('gcc'),
+ gn_args(pw_target_config='"//targets/host/host.gni"',
+ pw_target_toolchain='"//pw_toolchain:host_gcc_os"'))
+ call('ninja', '-C', presubmit_dir('gcc'))
+
+
+def gn_arm_build():
+ call(
+ *GN_GEN, presubmit_dir('arm'),
+ gn_args(
+ pw_target_config='"//targets/stm32f429i-disc1/target_config.gni"'))
+ call('ninja', '-C', presubmit_dir('arm'))
+
+
+GN = (
+ gn_format,
+ gn_clang_build,
+ gn_gcc_build,
+ gn_arm_build,
)
+#
+# C++ presubmit checks
+#
+@filter_paths(endswith=format_cc.SOURCE_EXTENSIONS)
+def clang_format(paths):
+ if format_cc.check_format(paths):
+ raise PresubmitFailure
+
+
+@filter_paths(endswith=format_cc.SOURCE_EXTENSIONS)
+def clang_tidy(paths):
+ if not os.path.exists(presubmit_dir('clang', 'compile_commands.json')):
+ raise PresubmitFailure('clang_tidy MUST be run after generating '
+ 'compile_commands.json in a clang build!')
+
+ call('clang-tidy', f'-p={presubmit_dir("clang")}', *paths)
+
+
+CC = (
+ presubmit_tools.pragma_once,
+ clang_format,
+ # TODO(hepler): Enable clang-tidy when it passes.
+ # clang_tidy,
+)
+
+
+#
+# Python presubmit checks
+#
+@filter_paths(endswith='.py')
+def pylint_errors(paths):
+ call(sys.executable, '-m', 'pylint', '-E', *paths)
+
+
+@filter_paths(endswith='.py')
+def yapf(paths):
+ from yapf.yapflib.yapf_api import FormatFile
+
+ errors = []
+
+ for path in paths:
+ diff, _, changed = FormatFile(path, print_diff=True, in_place=False)
+ if changed:
+ errors.append(path)
+ print(format_cc.colorize_diff(diff))
+
+ if errors:
+ print(f'--> Files with formatting errors: {len(errors)}')
+ print(' ', '\n '.join(errors))
+ raise PresubmitFailure
+
+
+@filter_paths(endswith='.py', exclude=r'(?:.+/)?setup\.py')
+def mypy(paths):
+ import mypy.api as mypy_api
+
+ report, errors, exit_status = mypy_api.run(paths)
+ if exit_status:
+ print(errors)
+ print(report)
+ raise PresubmitFailure
+
+
+PYTHON = (
+ # TODO(hepler): Enable yapf, mypy, and pylint when they pass.
+ # pylint_errors,
+ # yapf,
+ # mypy,
+)
+
+
+#
+# Bazel presubmit checks
+#
+@filter_paths(endswith=format_cc.SOURCE_EXTENSIONS)
+def bazel_test(unused_paths):
+ prefix = '.presubmit/bazel-'
+ call('bazel', 'build', '//...', '--symlink_prefix', prefix)
+ call('bazel', 'test', '//...', '--symlink_prefix', prefix)
+
+
+BAZEL = (bazel_test, )
+
+#
+# General presubmit checks
+#
+COPYRIGHT_FIRST_LINE = re.compile(
+ r'^(#|//| \*) Copyright 20\d\d The Pigweed Authors$')
+
+COPYRIGHT_LINES = tuple("""\
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ use this file except in compliance with the License. You may obtain a copy of
+ the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ License for the specific language governing permissions and limitations under
+ the License.
+""".splitlines(True))
+
+_EXCLUDE_FROM_COPYRIGHT_NOTICE = (
+ r'(?:.+/)?\..+',
+ r'AUTHORS',
+ r'LICENSE',
+ r'.*\.md',
+ r'.*\.rst',
+ r'(?:.+/)?requirements.txt',
+ r'(?:.+/)?requirements.in',
+)
+
+
+@filter_paths(exclude=_EXCLUDE_FROM_COPYRIGHT_NOTICE)
+def copyright_notice(paths):
+ """Checks that the copyright notice is present."""
+
+ errors = []
+
+ for path in paths:
+ with open(path) as file:
+ # Skip shebang and blank lines
+ line = file.readline()
+ while line.startswith(('#!', '/*')) or not line.strip():
+ line = file.readline()
+
+ first_line = COPYRIGHT_FIRST_LINE.match(line)
+ if not first_line:
+ errors.append(path)
+ continue
+
+ comment = first_line.group(1)
+
+ for expected, actual in zip(COPYRIGHT_LINES, file):
+ if comment + expected != actual:
+ errors.append(path)
+ break
+
+ if errors:
+ print('-->', presubmit_tools.plural(errors, 'file'),
+ 'with a missing or incorrect copyright notice:')
+ print(' ', '\n '.join(errors))
+ raise PresubmitFailure
+
+
+GENERAL = (copyright_notice, )
+
+#
+# Presubmit check programs
+#
+QUICK_PRESUBMIT = (
+ *GENERAL,
+ *PYTHON,
+ gn_format,
+ gn_clang_build,
+ presubmit_tools.pragma_once,
+ clang_format,
+)
+
+PROGRAMS = {
+ 'full': GN + CC + PYTHON + BAZEL + GENERAL,
+ 'quick': QUICK_PRESUBMIT,
+}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--clean',
+ action='store_true',
+ help='Deletes the .presubmit directory before starting')
+ parser.add_argument(
+ '--skip_init',
+ action='store_true',
+ help='Clone the buildtools to prior to running the checks')
+ parser.add_argument('-p',
+ '--program',
+ choices=PROGRAMS,
+ default='full',
+ help='Which presubmit program to run')
+
+ presubmit_tools.add_parser_arguments(parser)
+
+ args = parser.parse_args()
+
+ if args.clean and os.path.exists(presubmit_dir()):
+ shutil.rmtree(presubmit_dir())
+
+ init_step = () if args.skip_init else (init,)
+ program = init_step + PROGRAMS[args.program]
+
+ # Remove custom arguments so we can use args to call run_presubmit.
+ del args.clean, args.program, args.skip_init
+
+ return 0 if presubmit_tools.run_presubmit(program, **vars(args)) else 1
+
if __name__ == '__main__':
- sys.exit(0 if parse_args_and_run_presubmit(PRESUBMIT_PROGRAM) else 1)
+ sys.exit(main())