• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3# Copyright 2020 The Pigweed Authors
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6# use this file except in compliance with the License. You may obtain a copy of
7# the License at
8#
9#     https://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations under
15# the License.
16"""Runs the local presubmit checks for the Pigweed repository."""
17
18import argparse
19import logging
20import os
21from pathlib import Path
22import re
23import shutil
24import sys
25from typing import Sequence, IO, Tuple, Optional
26
27try:
28    import pw_presubmit
29except ImportError:
30    # Append the pw_presubmit package path to the module search path to allow
31    # running this module without installing the pw_presubmit package.
32    sys.path.append(os.path.dirname(os.path.dirname(
33        os.path.abspath(__file__))))
34    import pw_presubmit
35
36import pw_package.pigweed_packages
37
38from pw_presubmit import build, cli, format_code, git_repo
39from pw_presubmit import call, filter_paths, plural, PresubmitContext
40from pw_presubmit import PresubmitFailure, Programs
41from pw_presubmit.install_hook import install_hook
42
43_LOG = logging.getLogger(__name__)
44
45pw_package.pigweed_packages.initialize()
46
47# Trigger builds if files with these extensions change.
48_BUILD_EXTENSIONS = ('.py', '.rst', '.gn', '.gni',
49                     *format_code.C_FORMAT.extensions)
50
51
52def _at_all_optimization_levels(target):
53    levels = ('debug', 'size_optimized', 'speed_optimized')
54
55    # Skip optimized host GCC builds for now, since GCC sometimes emits spurious
56    # warnings.
57    #
58    #   -02: GCC 9.3 emits spurious maybe-uninitialized warnings
59    #   -0s: GCC 8.1 (Mingw-w64) emits a spurious nonnull warning
60    #
61    # TODO(pwbug/255): Enable optimized GCC builds when this is fixed.
62    if target == 'host_gcc':
63        levels = ('debug', )
64
65    for level in levels:
66        yield f'{target}_{level}'
67
68
69#
70# Build presubmit checks
71#
72def gn_clang_build(ctx: PresubmitContext):
73    build.gn_gen(ctx.root, ctx.output_dir)
74    build.ninja(ctx.output_dir, *_at_all_optimization_levels('host_clang'))
75
76
77@filter_paths(endswith=_BUILD_EXTENSIONS)
78def gn_gcc_build(ctx: PresubmitContext):
79    build.gn_gen(ctx.root, ctx.output_dir)
80    build.ninja(ctx.output_dir, *_at_all_optimization_levels('host_gcc'))
81
82
83_HOST_COMPILER = 'gcc' if sys.platform == 'win32' else 'clang'
84
85
86def gn_host_build(ctx: PresubmitContext):
87    build.gn_gen(ctx.root, ctx.output_dir)
88    build.ninja(ctx.output_dir,
89                *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'))
90
91
92@filter_paths(endswith=_BUILD_EXTENSIONS)
93def gn_quick_build_check(ctx: PresubmitContext):
94    build.gn_gen(ctx.root, ctx.output_dir)
95
96    # TODO(pwbug/255): Switch to optimized GCC builds when this is fixed.
97    # See comment in _at_all_optimization_levels() above for details.
98    optimization_level = 'size_optimized'
99    if _HOST_COMPILER == 'gcc':
100        optimization_level = 'debug'
101
102    build.ninja(ctx.output_dir, f'host_{_HOST_COMPILER}_{optimization_level}',
103                'stm32f429i_size_optimized', 'python.tests', 'python.lint')
104
105
106@filter_paths(endswith=_BUILD_EXTENSIONS)
107def gn_full_build_check(ctx: PresubmitContext):
108    build.gn_gen(ctx.root, ctx.output_dir)
109    build.ninja(ctx.output_dir, *_at_all_optimization_levels('stm32f429i'),
110                *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
111                'python.tests', 'python.lint', 'docs')
112
113
114@filter_paths(endswith=_BUILD_EXTENSIONS)
115def gn_full_qemu_check(ctx: PresubmitContext):
116    build.gn_gen(ctx.root, ctx.output_dir)
117    build.ninja(ctx.output_dir, *_at_all_optimization_levels('qemu_gcc'),
118                *_at_all_optimization_levels('qemu_clang'))
119
120
121@filter_paths(endswith=_BUILD_EXTENSIONS)
122def gn_arm_build(ctx: PresubmitContext):
123    build.gn_gen(ctx.root, ctx.output_dir)
124    build.ninja(ctx.output_dir, *_at_all_optimization_levels('stm32f429i'))
125
126
127@filter_paths(endswith=_BUILD_EXTENSIONS)
128def gn_nanopb_build(ctx: PresubmitContext):
129    build.install_package(ctx.package_root, 'nanopb')
130    build.gn_gen(ctx.root,
131                 ctx.output_dir,
132                 dir_pw_third_party_nanopb='"{}"'.format(ctx.package_root /
133                                                         'nanopb'))
134    build.ninja(
135        ctx.output_dir,
136        *_at_all_optimization_levels('stm32f429i'),
137        *_at_all_optimization_levels('host_clang'),
138    )
139
140
141@filter_paths(endswith=_BUILD_EXTENSIONS)
142def gn_teensy_build(ctx: PresubmitContext):
143    build.install_package(ctx.package_root, 'teensy')
144    build.gn_gen(ctx.root,
145                 ctx.output_dir,
146                 pw_arduino_build_CORE_PATH='"{}"'.format(str(
147                     ctx.package_root)),
148                 pw_arduino_build_CORE_NAME='teensy',
149                 pw_arduino_build_PACKAGE_NAME='teensy/avr',
150                 pw_arduino_build_BOARD='teensy40')
151    build.ninja(ctx.output_dir, *_at_all_optimization_levels('arduino'))
152
153
154@filter_paths(endswith=_BUILD_EXTENSIONS)
155def gn_qemu_build(ctx: PresubmitContext):
156    build.gn_gen(ctx.root, ctx.output_dir)
157    build.ninja(ctx.output_dir, *_at_all_optimization_levels('qemu_gcc'))
158
159
160@filter_paths(endswith=_BUILD_EXTENSIONS)
161def gn_qemu_clang_build(ctx: PresubmitContext):
162    build.gn_gen(ctx.root, ctx.output_dir)
163    build.ninja(ctx.output_dir, *_at_all_optimization_levels('qemu_clang'))
164
165
166def gn_docs_build(ctx: PresubmitContext):
167    build.gn_gen(ctx.root, ctx.output_dir)
168    build.ninja(ctx.output_dir, 'docs')
169
170
171def gn_host_tools(ctx: PresubmitContext):
172    build.gn_gen(ctx.root, ctx.output_dir, pw_build_HOST_TOOLS=True)
173    build.ninja(ctx.output_dir, 'host')
174
175
176@filter_paths(endswith=format_code.C_FORMAT.extensions)
177def oss_fuzz_build(ctx: PresubmitContext):
178    build.gn_gen(ctx.root, ctx.output_dir, pw_toolchain_OSS_FUZZ_ENABLED=True)
179    build.ninja(ctx.output_dir, "fuzzers")
180
181
182@filter_paths(endswith='.py')
183def python_checks(ctx: PresubmitContext):
184    build.gn_gen(ctx.root, ctx.output_dir)
185    build.ninja(
186        ctx.output_dir,
187        ':python.lint',
188        ':python.tests',
189        ':target_support_packages.lint',
190        ':target_support_packages.tests',
191    )
192
193
194def _run_cmake(ctx: PresubmitContext) -> None:
195    build.install_package(ctx.package_root, 'nanopb')
196
197    toolchain = ctx.root / 'pw_toolchain' / 'host_clang' / 'toolchain.cmake'
198    build.cmake(ctx.root,
199                ctx.output_dir,
200                f'-DCMAKE_TOOLCHAIN_FILE={toolchain}',
201                '-DCMAKE_EXPORT_COMPILE_COMMANDS=1',
202                f'-Ddir_pw_third_party_nanopb={ctx.package_root / "nanopb"}',
203                '-Dpw_third_party_nanopb_ADD_SUBDIRECTORY=ON',
204                env=build.env_with_clang_vars())
205
206
207@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.cmake',
208                        'CMakeLists.txt'))
209def cmake_tests(ctx: PresubmitContext):
210    _run_cmake(ctx)
211    build.ninja(ctx.output_dir, 'pw_apps', 'pw_run_tests.modules')
212
213
214# TODO: Slowly add modules here that work with bazel until all
215# modules are added. Then replace with //...
216_MODULES_THAT_WORK_WITH_BAZEL = [
217    '//pw_assert_basic/...',
218    '//pw_base64/...',
219    '//pw_build/...',
220    '//pw_chrono_stl/...',
221    '//pw_containers/...',
222    '//pw_cpu_exception/...',
223    '//pw_docgen/...',
224    '//pw_doctor/...',
225    '//pw_i2c/...',
226    '//pw_log/...',
227    '//pw_log_basic/...',
228    '//pw_polyfill/...',
229    '//pw_preprocessor/...',
230    '//pw_protobuf_compiler/...',
231    '//pw_span/...',
232    '//pw_status/...',
233    '//pw_sys_io/...',
234    '//pw_sys_io_baremetal_lm3s6965evb/...',
235    '//pw_sys_io_stdio/...',
236    '//pw_thread_stl/...',
237    '//pw_toolchain/...',
238    '//pw_varint/...',
239    '//pw_web_ui/...',
240]
241
242
243@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.bzl', 'BUILD'))
244def bazel_test(ctx: PresubmitContext):
245    """Runs bazel test on each bazel compatible module"""
246
247    try:
248        call('bazel',
249             'test',
250             *_MODULES_THAT_WORK_WITH_BAZEL,
251             '--verbose_failures',
252             '--verbose_explanations',
253             '--worker_verbose',
254             '--test_output=errors',
255             cwd=ctx.root,
256             env=build.env_with_clang_vars())
257    except:
258        _LOG.info('If the Bazel build inexplicably fails while the '
259                  'other builds are passing, try deleting the Bazel cache:\n'
260                  '    rm -rf ~/.cache/bazel')
261        raise
262
263
264@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.bzl', 'BUILD'))
265def bazel_build(ctx: PresubmitContext):
266    """Runs Bazel build on each Bazel compatible module"""
267    try:
268        call('bazel',
269             'build',
270             *_MODULES_THAT_WORK_WITH_BAZEL,
271             '--verbose_failures',
272             '--verbose_explanations',
273             '--worker_verbose',
274             cwd=ctx.root,
275             env=build.env_with_clang_vars())
276    except:
277        _LOG.info('If the Bazel build inexplicably fails while the '
278                  'other builds are passing, try deleting the Bazel cache:\n'
279                  '    rm -rf ~/.cache/bazel')
280        raise
281
282
283#
284# General presubmit checks
285#
286
287# TODO(pwbug/45) Probably want additional checks.
288_CLANG_TIDY_CHECKS = ('modernize-use-override', )
289
290
291@filter_paths(endswith=format_code.C_FORMAT.extensions)
292def clang_tidy(ctx: PresubmitContext):
293    build.gn_gen(ctx.root, ctx.output_dir, '--export-compile-commands')
294    build.ninja(ctx.output_dir)
295    build.ninja(ctx.output_dir, '-t', 'compdb', 'objcxx', 'cxx')
296
297    run_clang_tidy = None
298    for var in ('PW_PIGWEED_CIPD_INSTALL_DIR', 'PW_CIPD_INSTALL_DIR'):
299        if var in os.environ:
300            possibility = os.path.join(os.environ[var],
301                                       'share/clang/run-clang-tidy.py')
302            if os.path.isfile(possibility):
303                run_clang_tidy = possibility
304                break
305
306    checks = ','.join(_CLANG_TIDY_CHECKS)
307    call(
308        run_clang_tidy,
309        f'-p={ctx.output_dir}',
310        f'-checks={checks}',
311        # TODO(pwbug/45) not sure if this is needed.
312        # f'-extra-arg-before=-warnings-as-errors={checks}',
313        *ctx.paths)
314
315
316# The first line must be regex because of the '20\d\d' date
317COPYRIGHT_FIRST_LINE = r'Copyright 20\d\d The Pigweed Authors'
318COPYRIGHT_COMMENTS = r'(#|//| \*|REM|::)'
319COPYRIGHT_BLOCK_COMMENTS = (
320    # HTML comments
321    (r'<!--', r'-->'), )
322
323COPYRIGHT_FIRST_LINE_EXCEPTIONS = (
324    '#!',
325    '/*',
326    '@echo off',
327    '# -*-',
328    ':',
329)
330
331COPYRIGHT_LINES = tuple("""\
332
333Licensed under the Apache License, Version 2.0 (the "License"); you may not
334use this file except in compliance with the License. You may obtain a copy of
335the License at
336
337    https://www.apache.org/licenses/LICENSE-2.0
338
339Unless required by applicable law or agreed to in writing, software
340distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
341WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
342License for the specific language governing permissions and limitations under
343the License.
344""".splitlines())
345
346_EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
347    # Configuration
348    r'^(?:.+/)?\..+$',
349    r'\bPW_PLUGINS$',
350    # Metadata
351    r'^docker/tag$',
352    r'\bAUTHORS$',
353    r'\bLICENSE$',
354    r'\bOWNERS$',
355    r'\brequirements.txt$',
356    r'\bgo.(mod|sum)$',
357    r'\bpackage.json$',
358    r'\byarn.lock$',
359    # Data files
360    r'\.elf$',
361    r'\.gif$',
362    r'\.jpg$',
363    r'\.json$',
364    r'\.png$',
365    # Documentation
366    r'\.md$',
367    r'\.rst$',
368    # Generated protobuf files
369    r'\.pb\.h$',
370    r'\.pb\.c$',
371    r'\_pb2.pyi?$',
372    # Diff/Patch files
373    r'\.diff$',
374    r'\.patch$',
375)
376
377
378def match_block_comment_start(line: str) -> Optional[str]:
379    """Matches the start of a block comment and returns the end."""
380    for block_comment in COPYRIGHT_BLOCK_COMMENTS:
381        if re.match(block_comment[0], line):
382            # Return the end of the block comment
383            return block_comment[1]
384    return None
385
386
387def copyright_read_first_line(
388        file: IO) -> Tuple[Optional[str], Optional[str], Optional[str]]:
389    """Reads the file until it reads a valid first copyright line.
390
391    Returns (comment, block_comment, line). comment and block_comment are
392    mutually exclusive and refer to the comment character sequence and whether
393    they form a block comment or a line comment. line is the first line of
394    the copyright, and is used for error reporting.
395    """
396    line = file.readline()
397    first_line_matcher = re.compile(COPYRIGHT_COMMENTS + ' ' +
398                                    COPYRIGHT_FIRST_LINE)
399    while line:
400        end_block_comment = match_block_comment_start(line)
401        if end_block_comment:
402            next_line = file.readline()
403            copyright_line = re.match(COPYRIGHT_FIRST_LINE, next_line)
404            if not copyright_line:
405                return (None, None, line)
406            return (None, end_block_comment, line)
407
408        first_line = first_line_matcher.match(line)
409        if first_line:
410            return (first_line.group(1), None, line)
411
412        if (line.strip()
413                and not line.startswith(COPYRIGHT_FIRST_LINE_EXCEPTIONS)):
414            return (None, None, line)
415
416        line = file.readline()
417    return (None, None, None)
418
419
420@filter_paths(exclude=_EXCLUDE_FROM_COPYRIGHT_NOTICE)
421def copyright_notice(ctx: PresubmitContext):
422    """Checks that the Pigweed copyright notice is present."""
423    errors = []
424
425    for path in ctx.paths:
426
427        if path.stat().st_size == 0:
428            continue  # Skip empty files
429
430        with path.open() as file:
431            (comment, end_block_comment,
432             line) = copyright_read_first_line(file)
433
434            if not line:
435                _LOG.warning('%s: invalid first line', path)
436                errors.append(path)
437                continue
438
439            if not (comment or end_block_comment):
440                _LOG.warning('%s: invalid first line %r', path, line)
441                errors.append(path)
442                continue
443
444            if end_block_comment:
445                expected_lines = COPYRIGHT_LINES + (end_block_comment, )
446            else:
447                expected_lines = COPYRIGHT_LINES
448
449            for expected, actual in zip(expected_lines, file):
450                if end_block_comment:
451                    expected_line = expected + '\n'
452                elif comment:
453                    expected_line = (comment + ' ' + expected).rstrip() + '\n'
454
455                if expected_line != actual:
456                    _LOG.warning('  bad line: %r', actual)
457                    _LOG.warning('  expected: %r', expected_line)
458                    errors.append(path)
459                    break
460
461    if errors:
462        _LOG.warning('%s with a missing or incorrect copyright notice:\n%s',
463                     plural(errors, 'file'), '\n'.join(str(e) for e in errors))
464        raise PresubmitFailure
465
466
467_BAZEL_SOURCES_IN_BUILD = tuple(format_code.C_FORMAT.extensions)
468_GN_SOURCES_IN_BUILD = '.rst', '.py', *_BAZEL_SOURCES_IN_BUILD
469
470
471@filter_paths(endswith=(*_GN_SOURCES_IN_BUILD, 'BUILD', '.bzl', '.gn', '.gni'))
472def source_is_in_build_files(ctx: PresubmitContext):
473    """Checks that source files are in the GN and Bazel builds."""
474    missing = build.check_builds_for_files(
475        _BAZEL_SOURCES_IN_BUILD,
476        _GN_SOURCES_IN_BUILD,
477        ctx.paths,
478        bazel_dirs=[ctx.root],
479        gn_build_files=git_repo.list_files(pathspecs=['BUILD.gn', '*BUILD.gn'],
480                                           repo_path=ctx.root))
481
482    if missing:
483        _LOG.warning(
484            'All source files must appear in BUILD and BUILD.gn files')
485        raise PresubmitFailure
486
487    _run_cmake(ctx)
488    cmake_missing = build.check_compile_commands_for_files(
489        ctx.output_dir / 'compile_commands.json',
490        (f for f in ctx.paths if f.suffix in ('.c', '.cc')))
491    if cmake_missing:
492        _LOG.warning('The CMake build is missing %d files', len(cmake_missing))
493        _LOG.warning('Files missing from CMake:\n%s',
494                     '\n'.join(str(f) for f in cmake_missing))
495        # TODO(hepler): Many files are missing from the CMake build. Make this
496        #     check an error when the missing files are fixed.
497        # raise PresubmitFailure
498
499
500def build_env_setup(ctx: PresubmitContext):
501    if 'PW_CARGO_SETUP' not in os.environ:
502        _LOG.warning(
503            'Skipping build_env_setup since PW_CARGO_SETUP is not set')
504        return
505
506    tmpl = ctx.root.joinpath('pw_env_setup', 'py', 'pyoxidizer.bzl.tmpl')
507    out = ctx.output_dir.joinpath('pyoxidizer.bzl')
508
509    with open(tmpl, 'r') as ins:
510        cfg = ins.read().replace('${PW_ROOT}', str(ctx.root))
511        with open(out, 'w') as outs:
512            outs.write(cfg)
513
514    call('pyoxidizer', 'build', cwd=ctx.output_dir)
515
516
517def commit_message_format(_: PresubmitContext):
518    """Checks that the top commit's message is correctly formatted."""
519    lines = git_repo.commit_message().splitlines()
520
521    # Show limits and current commit message in log.
522    _LOG.debug('%-25s%+25s%+22s', 'Line limits', '72|', '72|')
523    for line in lines:
524        _LOG.debug(line)
525
526    # Ignore Gerrit-generated reverts.
527    if ('Revert' in lines[0]
528            and 'This reverts commit ' in git_repo.commit_message()
529            and 'Reason for revert: ' in git_repo.commit_message()):
530        _LOG.warning('Ignoring apparent Gerrit-generated revert')
531        return
532
533    if not lines:
534        _LOG.error('The commit message is too short!')
535        raise PresubmitFailure
536
537    errors = 0
538
539    if len(lines[0]) > 72:
540        _LOG.warning("The commit message's first line must be no longer than "
541                     '72 characters.')
542        _LOG.warning('The first line is %d characters:\n  %s', len(lines[0]),
543                     lines[0])
544        errors += 1
545
546    if lines[0].endswith('.'):
547        _LOG.warning(
548            "The commit message's first line must not end with a period:\n %s",
549            lines[0])
550        errors += 1
551
552    if len(lines) > 1 and lines[1]:
553        _LOG.warning("The commit message's second line must be blank.")
554        _LOG.warning('The second line has %d characters:\n  %s', len(lines[1]),
555                     lines[1])
556        errors += 1
557
558    # Check that the lines are 72 characters or less, but skip any lines that
559    # might possibly have a URL, path, or metadata in them. Also skip any lines
560    # with non-ASCII characters.
561    for i, line in enumerate(lines[2:], 3):
562        if any(c in line for c in ':/>') or not line.isascii():
563            continue
564
565        if len(line) > 72:
566            _LOG.warning(
567                'Commit message lines must be no longer than 72 characters.')
568            _LOG.warning('Line %d has %d characters:\n  %s', i, len(line),
569                         line)
570            errors += 1
571
572    if errors:
573        _LOG.error('Found %s in the commit message', plural(errors, 'error'))
574        raise PresubmitFailure
575
576
577def static_analysis(ctx: PresubmitContext):
578    """Check that files pass static analyzer checks."""
579    build.gn_gen(ctx.root, ctx.output_dir,
580                 '--export-compile-commands=host_clang_debug')
581    build.ninja(ctx.output_dir, 'host_clang_debug')
582
583    compile_commands = ctx.output_dir.joinpath('compile_commands.json')
584    analyzer_output = ctx.output_dir.joinpath('analyze-build-output')
585
586    if analyzer_output.exists():
587        shutil.rmtree(analyzer_output)
588
589    call('analyze-build',
590         '--cdb',
591         compile_commands,
592         '--exclude',
593         'third_party',
594         '--output',
595         analyzer_output,
596         cwd=ctx.root,
597         env=build.env_with_clang_vars())
598
599    # Search for reports under output directory.
600    reports = list(analyzer_output.glob('*/report*'))
601    if len(reports) != 0:
602        archive = shutil.make_archive(str(analyzer_output), 'zip',
603                                      reports[0].parent)
604        _LOG.error('Static analyzer found errors: %s', archive)
605        _LOG.error('To view report, open: %s',
606                   Path(reports[0]).parent.joinpath('index.html'))
607        raise PresubmitFailure
608
609
610def renode_check(ctx: PresubmitContext):
611    """Placeholder for future check."""
612    _LOG.info('%s %s', ctx.root, ctx.output_dir)
613
614
615#
616# Presubmit check programs
617#
618
619OTHER_CHECKS = (
620    # TODO(pwbug/45): Remove clang-tidy from OTHER_CHECKS when it passes.
621    clang_tidy,
622    # Build that attempts to duplicate the build OSS-Fuzz does. Currently
623    # failing.
624    oss_fuzz_build,
625    bazel_test,
626    cmake_tests,
627    gn_nanopb_build,
628    gn_full_build_check,
629    gn_full_qemu_check,
630    gn_clang_build,
631    gn_gcc_build,
632    renode_check,
633    static_analysis,
634)
635
636LINTFORMAT = (
637    commit_message_format,
638    copyright_notice,
639    format_code.presubmit_checks(),
640    pw_presubmit.pragma_once,
641    source_is_in_build_files,
642)
643
644QUICK = (
645    LINTFORMAT,
646    bazel_test,
647    gn_quick_build_check,
648    # TODO(pwbug/141): Re-enable CMake and Bazel for Mac after we have fixed the
649    # the clang issues. The problem is that all clang++ invocations need the
650    # two extra flags: "-nostdc++" and "${clang_prefix}/../lib/libc++.a".
651    cmake_tests if sys.platform != 'darwin' else (),
652)
653
654FULL = (
655    LINTFORMAT,
656    gn_host_build,
657    gn_arm_build,
658    gn_docs_build,
659    gn_host_tools,
660    bazel_build,
661    bazel_test,
662    # On Mac OS, system 'gcc' is a symlink to 'clang' by default, so skip GCC
663    # host builds on Mac for now. Skip it on Windows too, since gn_host_build
664    # already uses 'gcc' on Windows.
665    gn_gcc_build if sys.platform not in ('darwin', 'win32') else (),
666    # Windows doesn't support QEMU yet.
667    gn_qemu_build if sys.platform != 'win32' else (),
668    gn_qemu_clang_build if sys.platform != 'win32' else (),
669    source_is_in_build_files,
670    python_checks,
671    build_env_setup,
672    # Skip gn_teensy_build if running on Windows. The Teensycore installer is
673    # an exe that requires an admin role.
674    gn_teensy_build if sys.platform in ['linux', 'darwin'] else (),
675)
676
677PROGRAMS = Programs(
678    full=FULL,
679    lintformat=LINTFORMAT,
680    other_checks=OTHER_CHECKS,
681    quick=QUICK,
682)
683
684
685def parse_args() -> argparse.Namespace:
686    """Creates an argument parser and parses arguments."""
687
688    parser = argparse.ArgumentParser(description=__doc__)
689    cli.add_arguments(parser, PROGRAMS, 'quick')
690    parser.add_argument(
691        '--install',
692        action='store_true',
693        help='Install the presubmit as a Git pre-push hook and exit.')
694
695    return parser.parse_args()
696
697
698def run(install: bool, **presubmit_args) -> int:
699    """Entry point for presubmit."""
700
701    if install:
702        install_hook(__file__, 'pre-push',
703                     ['--base', 'origin/master..HEAD', '--program', 'quick'],
704                     Path.cwd())
705        return 0
706
707    return cli.run(**presubmit_args)
708
709
710def main() -> int:
711    """Run the presubmit for the Pigweed repository."""
712    return run(**vars(parse_args()))
713
714
715if __name__ == '__main__':
716    try:
717        # If pw_cli is available, use it to initialize logs.
718        from pw_cli import log
719
720        log.install(logging.INFO)
721    except ImportError:
722        # If pw_cli isn't available, display log messages like a simple print.
723        logging.basicConfig(format='%(message)s', level=logging.INFO)
724
725    sys.exit(main())
726