xref: /aosp_15_r20/external/pigweed/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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 json
20import logging
21import os
22from pathlib import Path
23import platform
24import re
25import shutil
26import subprocess
27import sys
28from typing import Callable, Iterable, Sequence, TextIO
29
30from pw_cli.plural import plural
31from pw_cli.file_filter import FileFilter
32import pw_package.pigweed_packages
33from pw_presubmit import (
34    bazel_checks,
35    build,
36    cli,
37    cpp_checks,
38    format_code,
39    git_repo,
40    gitmodules,
41    inclusive_language,
42    javascript_checks,
43    json_check,
44    keep_sorted,
45    module_owners,
46    npm_presubmit,
47    owners_checks,
48    python_checks,
49    shell_checks,
50    source_in_build,
51    todo_check,
52)
53from pw_presubmit.presubmit import (
54    Programs,
55    call,
56    filter_paths,
57)
58from pw_presubmit.presubmit_context import (
59    PresubmitContext,
60    PresubmitFailure,
61)
62from pw_presubmit.tools import log_run
63from pw_presubmit.install_hook import install_git_hook
64
65_LOG = logging.getLogger(__name__)
66
67pw_package.pigweed_packages.initialize()
68
69# Trigger builds if files with these extensions change.
70_BUILD_FILE_FILTER = FileFilter(
71    suffix=(
72        *format_code.C_FORMAT.extensions,
73        '.cfg',
74        '.py',
75        '.rst',
76        '.gn',
77        '.gni',
78        '.emb',
79    )
80)
81
82_OPTIMIZATION_LEVELS = 'debug', 'size_optimized', 'speed_optimized'
83
84
85def _at_all_optimization_levels(target):
86    for level in _OPTIMIZATION_LEVELS:
87        yield f'{target}_{level}'
88
89
90class PigweedGnGenNinja(build.GnGenNinja):
91    """Add Pigweed-specific defaults to GnGenNinja."""
92
93    def add_default_gn_args(self, args):
94        """Add project-specific default GN args to 'args'."""
95        args['pw_C_OPTIMIZATION_LEVELS'] = ('debug',)
96
97
98def build_bazel(*args, **kwargs) -> None:
99    build.bazel(
100        *args, use_remote_cache=True, strict_module_lockfile=True, **kwargs
101    )
102
103
104#
105# Build presubmit checks
106#
107gn_all = PigweedGnGenNinja(
108    name='gn_all',
109    path_filter=_BUILD_FILE_FILTER,
110    gn_args=dict(pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS),
111    ninja_targets=('all',),
112)
113
114
115def gn_clang_build(ctx: PresubmitContext):
116    """Checks all compile targets that rely on LLVM tooling."""
117    build_targets = [
118        *_at_all_optimization_levels('host_clang'),
119        'cpp20_compatibility',
120        'asan',
121        'tsan',
122        'ubsan',
123        'runtime_sanitizers',
124        # TODO: b/234876100 - msan will not work until the C++ standard library
125        # included in the sysroot has a variant built with msan.
126    ]
127
128    # clang-tidy doesn't run on Windows.
129    if sys.platform != 'win32':
130        build_targets.append('static_analysis')
131
132    # QEMU doesn't run on Windows.
133    if sys.platform != 'win32':
134        # TODO: b/244604080 - For the pw::InlineString tests, qemu_clang_debug
135        #     and qemu_clang_speed_optimized produce a binary too large for the
136        #     QEMU target's 256KB flash. Restore debug and speed optimized
137        #     builds when this is fixed.
138        build_targets.append('qemu_clang_size_optimized')
139
140    # TODO: b/240982565 - SocketStream currently requires Linux.
141    if sys.platform.startswith('linux'):
142        build_targets.append('integration_tests')
143
144    build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
145    build.ninja(ctx, *build_targets)
146    build.gn_check(ctx)
147
148
149_HOST_COMPILER = 'gcc' if sys.platform == 'win32' else 'clang'
150
151
152@filter_paths(file_filter=_BUILD_FILE_FILTER)
153def gn_quick_build_check(ctx: PresubmitContext):
154    """Checks the state of the GN build by running gn gen and gn check."""
155    build.gn_gen(ctx)
156
157
158def _gn_main_build_check_targets() -> Sequence[str]:
159    build_targets = [
160        'check_modules',
161        *_at_all_optimization_levels('stm32f429i'),
162        *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
163        'python.tests',
164        'python.lint',
165        'pigweed_pypi_distribution',
166    ]
167
168    # Since there is no mac-arm64 bloaty binary in CIPD, Arm Macs use the x86_64
169    # binary. However, Arm Macs in Pigweed CI disable Rosetta 2, so skip the
170    # 'default' build on those machines for now.
171    #
172    # TODO: b/368387791 - Add 'default' for all platforms when Arm Mac bloaty is
173    # available.
174    if platform.machine() != 'arm64' or sys.platform != 'darwin':
175        build_targets.append('default')
176
177    return build_targets
178
179
180def _gn_platform_build_check_targets() -> Sequence[str]:
181    build_targets = []
182
183    # TODO: b/315998985 - Add docs back to Mac ARM build.
184    if sys.platform != 'darwin' or platform.machine() != 'arm64':
185        build_targets.append('docs')
186
187    # C headers seem to be missing when building with pw_minimal_cpp_stdlib, so
188    # skip it on Windows.
189    if sys.platform != 'win32':
190        build_targets.append('build_with_pw_minimal_cpp_stdlib')
191
192    # TODO: b/234645359 - Re-enable on Windows when compatibility tests build.
193    if sys.platform != 'win32':
194        build_targets.append('cpp20_compatibility')
195
196    # clang-tidy doesn't run on Windows.
197    if sys.platform != 'win32':
198        build_targets.append('static_analysis')
199
200    # QEMU doesn't run on Windows.
201    if sys.platform != 'win32':
202        # TODO: b/244604080 - For the pw::InlineString tests, qemu_*_debug
203        #     and qemu_*_speed_optimized produce a binary too large for the
204        #     QEMU target's 256KB flash. Restore debug and speed optimized
205        #     builds when this is fixed.
206        build_targets.append('qemu_gcc_size_optimized')
207        build_targets.append('qemu_clang_size_optimized')
208
209    # TODO: b/240982565 - SocketStream currently requires Linux.
210    if sys.platform.startswith('linux'):
211        build_targets.append('integration_tests')
212
213    # TODO: b/269354373 - clang is not supported on windows yet
214    if sys.platform != 'win32':
215        build_targets.append('host_clang_debug_dynamic_allocation')
216
217    return build_targets
218
219
220def _gn_combined_build_check_targets() -> Sequence[str]:
221    return [
222        *_gn_main_build_check_targets(),
223        *_gn_platform_build_check_targets(),
224    ]
225
226
227gn_main_build_check = PigweedGnGenNinja(
228    name='gn_main_build_check',
229    doc='Run most host.',
230    path_filter=_BUILD_FILE_FILTER,
231    gn_args=dict(
232        pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
233        pw_BUILD_BROKEN_GROUPS=True,  # Enable to fully test the GN build
234    ),
235    ninja_targets=_gn_main_build_check_targets(),
236)
237
238gn_platform_build_check = PigweedGnGenNinja(
239    name='gn_platform_build_check',
240    doc='Run any host platform-specific tests.',
241    path_filter=_BUILD_FILE_FILTER,
242    gn_args=dict(
243        pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
244        pw_BUILD_BROKEN_GROUPS=True,  # Enable to fully test the GN build
245    ),
246    ninja_targets=_gn_platform_build_check_targets(),
247)
248
249gn_combined_build_check = PigweedGnGenNinja(
250    name='gn_combined_build_check',
251    doc='Run most host and device (QEMU) tests.',
252    path_filter=_BUILD_FILE_FILTER,
253    packages=('emboss',),
254    gn_args=dict(
255        pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
256        pw_BUILD_BROKEN_GROUPS=True,  # Enable to fully test the GN build
257    ),
258    ninja_targets=_gn_combined_build_check_targets(),
259)
260
261coverage = PigweedGnGenNinja(
262    name='coverage',
263    doc='Run coverage for the host build.',
264    path_filter=_BUILD_FILE_FILTER,
265    ninja_targets=('coverage',),
266    coverage_options=build.CoverageOptions(
267        common=build.CommonCoverageOptions(
268            target_bucket_project='pigweed',
269            target_bucket_root='gs://ng3-metrics/ng3-pigweed-coverage',
270            trace_type='LLVM',
271            owner='[email protected]',
272            bug_component='503634',
273        ),
274        codesearch=(
275            build.CodeSearchCoverageOptions(
276                host='pigweed-internal',
277                project='codesearch',
278                add_prefix='pigweed',
279                ref='refs/heads/main',
280                source='infra:main',
281            ),
282        ),
283        gerrit=build.GerritCoverageOptions(
284            project='pigweed/pigweed',
285        ),
286    ),
287)
288
289
290@filter_paths(file_filter=_BUILD_FILE_FILTER)
291def gn_arm_build(ctx: PresubmitContext):
292    build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
293    build.ninja(ctx, *_at_all_optimization_levels('stm32f429i'))
294    build.gn_check(ctx)
295
296
297stm32f429i = PigweedGnGenNinja(
298    name='stm32f429i',
299    path_filter=_BUILD_FILE_FILTER,
300    gn_args={
301        'pw_use_test_server': True,
302        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
303    },
304    ninja_contexts=(
305        lambda ctx: build.test_server(
306            'stm32f429i_disc1_test_server',
307            ctx.output_dir,
308        ),
309    ),
310    ninja_targets=_at_all_optimization_levels('stm32f429i'),
311)
312
313gn_crypto_mbedtls_build = PigweedGnGenNinja(
314    name='gn_crypto_mbedtls_build',
315    path_filter=_BUILD_FILE_FILTER,
316    packages=('mbedtls',),
317    gn_args={
318        'dir_pw_third_party_mbedtls': lambda ctx: '"{}"'.format(
319            ctx.package_root / 'mbedtls'
320        ),
321        'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
322            ctx.root / 'pw_crypto:sha256_mbedtls_v3'
323        ),
324        'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
325            ctx.root / 'pw_crypto:ecdsa_mbedtls_v3'
326        ),
327        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
328    },
329    ninja_targets=(
330        *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
331        # TODO: b/240982565 - SocketStream currently requires Linux.
332        *(('integration_tests',) if sys.platform.startswith('linux') else ()),
333    ),
334)
335
336gn_crypto_micro_ecc_build = PigweedGnGenNinja(
337    name='gn_crypto_micro_ecc_build',
338    path_filter=_BUILD_FILE_FILTER,
339    packages=('micro-ecc',),
340    gn_args={
341        'dir_pw_third_party_micro_ecc': lambda ctx: '"{}"'.format(
342            ctx.package_root / 'micro-ecc'
343        ),
344        'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
345            ctx.root / 'pw_crypto:ecdsa_uecc'
346        ),
347        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
348    },
349    ninja_targets=(
350        *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
351        # TODO: b/240982565 - SocketStream currently requires Linux.
352        *(('integration_tests',) if sys.platform.startswith('linux') else ()),
353    ),
354)
355
356gn_teensy_build = PigweedGnGenNinja(
357    name='gn_teensy_build',
358    path_filter=_BUILD_FILE_FILTER,
359    packages=('teensy',),
360    gn_args={
361        'pw_arduino_build_CORE_PATH': lambda ctx: '"{}"'.format(
362            str(ctx.package_root)
363        ),
364        'pw_arduino_build_CORE_NAME': 'teensy',
365        'pw_arduino_build_PACKAGE_NAME': 'avr/1.58.1',
366        'pw_arduino_build_BOARD': 'teensy40',
367        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
368    },
369    ninja_targets=_at_all_optimization_levels('arduino'),
370)
371
372gn_pico_build = PigweedGnGenNinja(
373    name='gn_pico_build',
374    path_filter=_BUILD_FILE_FILTER,
375    packages=('pico_sdk', 'freertos', 'emboss'),
376    gn_args={
377        'dir_pw_third_party_emboss': lambda ctx: '"{}"'.format(
378            str(ctx.package_root / 'emboss')
379        ),
380        'dir_pw_third_party_freertos': lambda ctx: '"{}"'.format(
381            str(ctx.package_root / 'freertos')
382        ),
383        'PICO_SRC_DIR': lambda ctx: '"{}"'.format(
384            str(ctx.package_root / 'pico_sdk')
385        ),
386        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
387    },
388    ninja_targets=('pi_pico',),
389)
390
391gn_mimxrt595_build = PigweedGnGenNinja(
392    name='gn_mimxrt595_build',
393    path_filter=_BUILD_FILE_FILTER,
394    packages=('mcuxpresso',),
395    gn_args={
396        'dir_pw_third_party_mcuxpresso': lambda ctx: '"{}"'.format(
397            str(ctx.package_root / 'mcuxpresso')
398        ),
399        'pw_target_mimxrt595_evk_MANIFEST': '$dir_pw_third_party_mcuxpresso'
400        + '/EVK-MIMXRT595_manifest_v3_13.xml',
401        'pw_third_party_mcuxpresso_SDK': '//targets/mimxrt595_evk:sample_sdk',
402        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
403    },
404    ninja_targets=('mimxrt595'),
405)
406
407gn_mimxrt595_freertos_build = PigweedGnGenNinja(
408    name='gn_mimxrt595_freertos_build',
409    path_filter=_BUILD_FILE_FILTER,
410    packages=('freertos', 'mcuxpresso'),
411    gn_args={
412        'dir_pw_third_party_freertos': lambda ctx: '"{}"'.format(
413            str(ctx.package_root / 'freertos')
414        ),
415        'dir_pw_third_party_mcuxpresso': lambda ctx: '"{}"'.format(
416            str(ctx.package_root / 'mcuxpresso')
417        ),
418        'pw_target_mimxrt595_evk_freertos_MANIFEST': '{}/{}'.format(
419            "$dir_pw_third_party_mcuxpresso", "EVK-MIMXRT595_manifest_v3_13.xml"
420        ),
421        'pw_third_party_mcuxpresso_SDK': '//targets/mimxrt595_evk_freertos:sdk',
422        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
423    },
424    ninja_targets=('mimxrt595_freertos'),
425)
426
427gn_software_update_build = PigweedGnGenNinja(
428    name='gn_software_update_build',
429    path_filter=_BUILD_FILE_FILTER,
430    packages=('nanopb', 'protobuf', 'mbedtls', 'micro-ecc'),
431    gn_args={
432        'dir_pw_third_party_protobuf': lambda ctx: '"{}"'.format(
433            ctx.package_root / 'protobuf'
434        ),
435        'dir_pw_third_party_nanopb': lambda ctx: '"{}"'.format(
436            ctx.package_root / 'nanopb'
437        ),
438        'dir_pw_third_party_micro_ecc': lambda ctx: '"{}"'.format(
439            ctx.package_root / 'micro-ecc'
440        ),
441        'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
442            ctx.root / 'pw_crypto:ecdsa_uecc'
443        ),
444        'dir_pw_third_party_mbedtls': lambda ctx: '"{}"'.format(
445            ctx.package_root / 'mbedtls'
446        ),
447        'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
448            ctx.root / 'pw_crypto:sha256_mbedtls_v3'
449        ),
450        'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
451    },
452    ninja_targets=_at_all_optimization_levels('host_clang'),
453)
454
455gn_pw_system_demo_build = PigweedGnGenNinja(
456    name='gn_pw_system_demo_build',
457    path_filter=_BUILD_FILE_FILTER,
458    packages=('freertos', 'nanopb', 'stm32cube_f4', 'pico_sdk'),
459    gn_args={
460        'dir_pw_third_party_freertos': lambda ctx: '"{}"'.format(
461            ctx.package_root / 'freertos'
462        ),
463        'dir_pw_third_party_nanopb': lambda ctx: '"{}"'.format(
464            ctx.package_root / 'nanopb'
465        ),
466        'dir_pw_third_party_stm32cube_f4': lambda ctx: '"{}"'.format(
467            ctx.package_root / 'stm32cube_f4'
468        ),
469        'PICO_SRC_DIR': lambda ctx: '"{}"'.format(
470            str(ctx.package_root / 'pico_sdk')
471        ),
472    },
473    ninja_targets=('pw_system_demo',),
474)
475
476gn_chre_googletest_nanopb_sapphire_build = PigweedGnGenNinja(
477    name='gn_chre_googletest_nanopb_sapphire_build',
478    path_filter=_BUILD_FILE_FILTER,
479    packages=('boringssl', 'chre', 'emboss', 'googletest', 'nanopb'),
480    gn_args=dict(
481        dir_pw_third_party_chre=lambda ctx: '"{}"'.format(
482            ctx.package_root / 'chre'
483        ),
484        dir_pw_third_party_nanopb=lambda ctx: '"{}"'.format(
485            ctx.package_root / 'nanopb'
486        ),
487        dir_pw_third_party_googletest=lambda ctx: '"{}"'.format(
488            ctx.package_root / 'googletest'
489        ),
490        dir_pw_third_party_emboss=lambda ctx: '"{}"'.format(
491            ctx.package_root / 'emboss'
492        ),
493        dir_pw_third_party_boringssl=lambda ctx: '"{}"'.format(
494            ctx.package_root / 'boringssl'
495        ),
496        pw_unit_test_MAIN=lambda ctx: '"{}"'.format(
497            ctx.root / 'third_party/googletest:gmock_main'
498        ),
499        pw_unit_test_BACKEND=lambda ctx: '"{}"'.format(
500            ctx.root / 'pw_unit_test:googletest'
501        ),
502        pw_function_CONFIG=lambda ctx: '"{}"'.format(
503            ctx.root / 'pw_function:enable_dynamic_allocation'
504        ),
505        pw_bluetooth_sapphire_ENABLED=True,
506        pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
507    ),
508    ninja_targets=(
509        *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
510        *_at_all_optimization_levels('stm32f429i'),
511    ),
512)
513
514gn_fuzz_build = PigweedGnGenNinja(
515    name='gn_fuzz_build',
516    path_filter=_BUILD_FILE_FILTER,
517    packages=('abseil-cpp', 'fuzztest', 'googletest', 're2'),
518    gn_args={
519        'dir_pw_third_party_abseil_cpp': lambda ctx: '"{}"'.format(
520            ctx.package_root / 'abseil-cpp'
521        ),
522        'dir_pw_third_party_fuzztest': lambda ctx: '"{}"'.format(
523            ctx.package_root / 'fuzztest'
524        ),
525        'dir_pw_third_party_googletest': lambda ctx: '"{}"'.format(
526            ctx.package_root / 'googletest'
527        ),
528        'dir_pw_third_party_re2': lambda ctx: '"{}"'.format(
529            ctx.package_root / 're2'
530        ),
531        'pw_unit_test_MAIN': lambda ctx: '"{}"'.format(
532            ctx.root / 'third_party/googletest:gmock_main'
533        ),
534        'pw_unit_test_BACKEND': lambda ctx: '"{}"'.format(
535            ctx.root / 'pw_unit_test:googletest'
536        ),
537    },
538    ninja_targets=('fuzzers',),
539    ninja_contexts=(
540        lambda ctx: build.modified_env(
541            FUZZTEST_PRNG_SEED=build.fuzztest_prng_seed(ctx),
542        ),
543    ),
544)
545
546oss_fuzz_build = PigweedGnGenNinja(
547    name='oss_fuzz_build',
548    path_filter=_BUILD_FILE_FILTER,
549    packages=('abseil-cpp', 'fuzztest', 'googletest', 're2'),
550    gn_args={
551        'dir_pw_third_party_abseil_cpp': lambda ctx: '"{}"'.format(
552            ctx.package_root / 'abseil-cpp'
553        ),
554        'dir_pw_third_party_fuzztest': lambda ctx: '"{}"'.format(
555            ctx.package_root / 'fuzztest'
556        ),
557        'dir_pw_third_party_googletest': lambda ctx: '"{}"'.format(
558            ctx.package_root / 'googletest'
559        ),
560        'dir_pw_third_party_re2': lambda ctx: '"{}"'.format(
561            ctx.package_root / 're2'
562        ),
563        'pw_toolchain_OSS_FUZZ_ENABLED': True,
564    },
565    ninja_targets=('oss_fuzz',),
566)
567
568
569def _env_with_zephyr_vars(ctx: PresubmitContext) -> dict:
570    """Returns the environment variables with ... set for Zephyr."""
571    env = os.environ.copy()
572    # Set some variables here.
573    env['ZEPHYR_BASE'] = str(ctx.package_root / 'zephyr')
574    env['ZEPHYR_MODULES'] = str(ctx.root)
575    env['ZEPHYR_TOOLCHAIN_VARIANT'] = 'llvm'
576    return env
577
578
579def zephyr_build(ctx: PresubmitContext) -> None:
580    """Run Zephyr compatible tests"""
581    # Install the Zephyr package
582    build.install_package(ctx, 'zephyr')
583    # Configure the environment
584    env = _env_with_zephyr_vars(ctx)
585    # Get the python twister runner
586    twister = ctx.package_root / 'zephyr' / 'scripts' / 'twister'
587    # Get a list of the test roots
588    testsuite_roots = [
589        ctx.pw_root / dir
590        for dir in os.listdir(ctx.pw_root)
591        if dir.startswith('pw_')
592    ]
593    testsuite_roots_list = [
594        args for dir in testsuite_roots for args in ('--testsuite-root', dir)
595    ]
596    sysroot_dir = (
597        ctx.pw_root
598        / 'environment'
599        / 'cipd'
600        / 'packages'
601        / 'pigweed'
602        / 'clang_sysroot'
603    )
604    platform_filters = (
605        ['-P', 'native_posix', '-P', 'native_sim']
606        if platform.system() in ['Windows', 'Darwin']
607        else []
608    )
609    # Run twister
610    call(
611        sys.executable,
612        twister,
613        '--ninja',
614        '--integration',
615        '--clobber-output',
616        '--inline-logs',
617        '--verbose',
618        *platform_filters,
619        '-x=CONFIG_LLVM_USE_LLD=y',
620        '-x=CONFIG_COMPILER_RT_RTLIB=y',
621        f'-x=TOOLCHAIN_C_FLAGS=--sysroot={sysroot_dir}',
622        f'-x=TOOLCHAIN_LD_FLAGS=--sysroot={sysroot_dir}',
623        *testsuite_roots_list,
624        env=env,
625    )
626    # Produces reports at (ctx.root / 'twister_out' / 'twister*.xml')
627
628
629def assert_non_empty_directory(directory: Path) -> None:
630    if not directory.is_dir():
631        raise PresubmitFailure(f'no directory {directory}')
632
633    for _ in directory.iterdir():
634        return
635
636    raise PresubmitFailure(f'no files in {directory}')
637
638
639def docs_build(ctx: PresubmitContext) -> None:
640    """Build Pigweed docs"""
641    if ctx.dry_run:
642        raise PresubmitFailure(
643            'This presubmit cannot be run in dry-run mode. '
644            'Please run with: "pw presubmit --step"'
645        )
646
647    build.install_package(ctx, 'emboss')
648    build.install_package(ctx, 'boringssl')
649    build.install_package(ctx, 'freertos')
650    build.install_package(ctx, 'nanopb')
651    build.install_package(ctx, 'pico_sdk')
652    build.install_package(ctx, 'pigweed_examples_repo')
653    build.install_package(ctx, 'stm32cube_f4')
654    emboss_dir = ctx.package_root / 'emboss'
655    boringssl_dir = ctx.package_root / 'boringssl'
656    pico_sdk_dir = ctx.package_root / 'pico_sdk'
657    stm32cube_dir = ctx.package_root / 'stm32cube_f4'
658    freertos_dir = ctx.package_root / 'freertos'
659    nanopb_dir = ctx.package_root / 'nanopb'
660
661    enable_dynamic_allocation = (
662        ctx.root / 'pw_function:enable_dynamic_allocation'
663    )
664
665    # Build main docs through GN/Ninja.
666    build.gn_gen(
667        ctx,
668        dir_pw_third_party_emboss=f'"{emboss_dir}"',
669        dir_pw_third_party_boringssl=f'"{boringssl_dir}"',
670        pw_bluetooth_sapphire_ENABLED=True,
671        pw_function_CONFIG=f'"{enable_dynamic_allocation}"',
672        pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
673    )
674    build.ninja(ctx, 'docs')
675    build.gn_check(ctx)
676
677    # Build Rust docs through Bazel.
678    build_bazel(
679        ctx,
680        'build',
681        '--remote_download_outputs=all',
682        '--',
683        '//pw_rust:docs',
684    )
685
686    # Build examples repo docs through GN.
687    examples_repo_root = ctx.package_root / 'pigweed_examples_repo'
688    examples_repo_out = examples_repo_root / 'out'
689
690    # Setup an examples repo presubmit context.
691    examples_ctx = PresubmitContext(
692        root=examples_repo_root,
693        repos=(examples_repo_root,),
694        output_dir=examples_repo_out,
695        failure_summary_log=ctx.failure_summary_log,
696        paths=tuple(),
697        all_paths=tuple(),
698        package_root=ctx.package_root,
699        luci=None,
700        override_gn_args={},
701        num_jobs=ctx.num_jobs,
702        continue_after_build_error=True,
703        _failed=False,
704        format_options=ctx.format_options,
705    )
706
707    # Write a pigweed_environment.gni for the examples repo.
708    pwenvgni = (
709        ctx.root / 'build_overrides/pigweed_environment.gni'
710    ).read_text()
711    # Fix the path for cipd packages.
712    pwenvgni.replace('../environment/cipd/', '../../cipd/')
713    # Write the file
714    (examples_repo_root / 'build_overrides/pigweed_environment.gni').write_text(
715        pwenvgni
716    )
717
718    # Set required GN args.
719    build.gn_gen(
720        examples_ctx,
721        dir_pigweed='"//../../.."',
722        dir_pw_third_party_stm32cube_f4=f'"{stm32cube_dir}"',
723        dir_pw_third_party_freertos=f'"{freertos_dir}"',
724        dir_pw_third_party_nanopb=f'"{nanopb_dir}"',
725        PICO_SRC_DIR=f'"{pico_sdk_dir}"',
726    )
727    build.ninja(examples_ctx, 'docs')
728
729    # Copy rust docs from Bazel's out directory into where the GN build
730    # put the main docs.
731    rust_docs_bazel_dir = ctx.output_dir / 'bazel-bin/pw_rust/docs.rustdoc'
732    rust_docs_output_dir = ctx.output_dir / 'docs/gen/docs/html/rustdoc'
733
734    # Copy the doxygen html output to the main docs location.
735    doxygen_html_gn_dir = ctx.output_dir / 'docs/doxygen/html'
736    doxygen_html_output_dir = ctx.output_dir / 'docs/gen/docs/html/doxygen'
737
738    # Copy the examples repo html output to the main docs location into
739    # '/examples/'.
740    examples_html_gn_dir = examples_repo_out / 'docs/gen/docs/html'
741    examples_html_output_dir = ctx.output_dir / 'docs/gen/docs/html/examples'
742
743    # Remove outputs to avoid including stale files from previous runs.
744    shutil.rmtree(rust_docs_output_dir, ignore_errors=True)
745    shutil.rmtree(doxygen_html_output_dir, ignore_errors=True)
746    shutil.rmtree(examples_html_output_dir, ignore_errors=True)
747
748    # Bazel generates files and directories without write permissions.  In
749    # order to allow this rule to be run multiple times we use shutil.copyfile
750    # for the actual copies to not copy permissions of files.
751    shutil.copytree(
752        rust_docs_bazel_dir,
753        rust_docs_output_dir,
754        copy_function=shutil.copyfile,
755        dirs_exist_ok=True,
756    )
757    assert_non_empty_directory(rust_docs_output_dir)
758
759    # Copy doxygen html outputs.
760    shutil.copytree(
761        doxygen_html_gn_dir,
762        doxygen_html_output_dir,
763        copy_function=shutil.copyfile,
764        dirs_exist_ok=True,
765    )
766    assert_non_empty_directory(doxygen_html_output_dir)
767
768    # mkdir -p the example repo output dir and copy the files over.
769    examples_html_output_dir.mkdir(parents=True, exist_ok=True)
770    shutil.copytree(
771        examples_html_gn_dir,
772        examples_html_output_dir,
773        copy_function=shutil.copyfile,
774        dirs_exist_ok=True,
775    )
776    assert_non_empty_directory(examples_html_output_dir)
777
778
779gn_host_tools = PigweedGnGenNinja(
780    name='gn_host_tools',
781    ninja_targets=('host_tools',),
782)
783
784
785def _run_cmake(ctx: PresubmitContext, toolchain='host_clang') -> None:
786    build.install_package(ctx, 'nanopb')
787    build.install_package(ctx, 'emboss')
788
789    env = None
790    if 'clang' in toolchain:
791        env = build.env_with_clang_vars()
792
793    toolchain_path = ctx.root / 'pw_toolchain' / toolchain / 'toolchain.cmake'
794    build.cmake(
795        ctx,
796        '--fresh',
797        f'-DCMAKE_TOOLCHAIN_FILE={toolchain_path}',
798        '-DCMAKE_EXPORT_COMPILE_COMMANDS=1',
799        f'-Ddir_pw_third_party_nanopb={ctx.package_root / "nanopb"}',
800        '-Dpw_third_party_nanopb_ADD_SUBDIRECTORY=ON',
801        f'-Ddir_pw_third_party_emboss={ctx.package_root / "emboss"}',
802        env=env,
803    )
804
805
806CMAKE_TARGETS = [
807    'pw_apps',
808    'pw_run_tests.modules',
809]
810
811
812@filter_paths(
813    endswith=(*format_code.C_FORMAT.extensions, '.cmake', 'CMakeLists.txt')
814)
815def cmake_clang(ctx: PresubmitContext):
816    _run_cmake(ctx, toolchain='host_clang')
817    build.ninja(ctx, *CMAKE_TARGETS)
818    build.gn_check(ctx)
819
820
821@filter_paths(
822    endswith=(*format_code.C_FORMAT.extensions, '.cmake', 'CMakeLists.txt')
823)
824def cmake_gcc(ctx: PresubmitContext):
825    _run_cmake(ctx, toolchain='host_gcc')
826    build.ninja(ctx, *CMAKE_TARGETS)
827    build.gn_check(ctx)
828
829
830@filter_paths(
831    endswith=(*format_code.C_FORMAT.extensions, '.bazel', '.bzl', 'BUILD')
832)
833def bazel_test(ctx: PresubmitContext) -> None:
834    """Runs bazel test on the entire repo."""
835    build_bazel(
836        ctx,
837        'test',
838        '--config=cxx20',
839        '--',
840        '//...',
841    )
842
843    # Run tests for non-default config options
844
845    # pw_rpc
846    build_bazel(
847        ctx,
848        'test',
849        '--//pw_rpc:config_override='
850        '//pw_rpc:completion_request_callback_config_enabled',
851        '--',
852        '//pw_rpc/...',
853    )
854
855    # pw_grpc
856    build_bazel(
857        ctx,
858        'test',
859        '--//pw_rpc:config_override=//pw_grpc:pw_rpc_config',
860        '--',
861        '//pw_grpc/...',
862    )
863
864
865def bthost_package(ctx: PresubmitContext) -> None:
866    """Builds, tests, and prepares bt_host for upload."""
867    target = '//pw_bluetooth_sapphire/fuchsia:infra'
868    build_bazel(ctx, 'build', '--config=fuchsia', target)
869
870    # Explicitly specify TEST_UNDECLARED_OUTPUTS_DIR_OVERRIDE as that will allow
871    # `orchestrate`'s output (eg: ffx host + target logs, test stdout/stderr) to
872    # be picked up by the `save_logs` recipe module.
873    # We cannot rely on Bazel's native TEST_UNDECLARED_OUTPUTS_DIR functionality
874    # since `zip` is not available in builders. See https://pwbug.dev/362990622.
875    build_bazel(
876        ctx,
877        'run',
878        '--config=fuchsia',
879        f'{target}.test_all',
880        env=dict(
881            os.environ,
882            TEST_UNDECLARED_OUTPUTS_DIR_OVERRIDE=ctx.output_dir,
883        ),
884    )
885
886    stdout_path = ctx.output_dir / 'bazel.manifest.stdout'
887    with open(stdout_path, 'w') as outs:
888        build_bazel(
889            ctx,
890            'build',
891            '--config=fuchsia',
892            '--output_groups=builder_manifest',
893            target,
894            stdout=outs,
895        )
896
897    manifest_path: Path | None = None
898    for line in stdout_path.read_text().splitlines():
899        line = line.strip()
900        if line.endswith('infrabuilder_manifest.json'):
901            manifest_path = Path(line)
902            break
903    else:
904        raise PresubmitFailure('no manifest found in output')
905
906    _LOG.debug('manifest: %s', manifest_path)
907    shutil.copyfile(manifest_path, ctx.output_dir / 'builder_manifest.json')
908
909
910@filter_paths(
911    endswith=(
912        *format_code.C_FORMAT.extensions,
913        '.bazel',
914        '.bzl',
915        '.py',
916        '.rs',
917        'BUILD',
918    )
919)
920def bazel_build(ctx: PresubmitContext) -> None:
921    """Runs Bazel build for each supported platform."""
922    # Build everything with the default flags.
923    build_bazel(
924        ctx,
925        'build',
926        '--',
927        '//...',
928    )
929
930    # Mapping from Bazel platforms to targets which should be built for those
931    # platforms.
932    targets_for_config = {
933        "lm3s6965evb": [
934            "//pw_rust/...",
935        ],
936        "microbit": [
937            "//pw_rust/...",
938        ],
939    }
940
941    for cxxversion in ('17', '20'):
942        # Explicitly build for each supported C++ version.
943        args = [ctx, 'build', f"--//pw_toolchain/cc:cxx_standard={cxxversion}"]
944        args += ['--', '//...']
945        build_bazel(*args)
946
947        for config, targets in targets_for_config.items():
948            build_bazel(
949                ctx,
950                'build',
951                f'--config={config}',
952                f"--//pw_toolchain/cc:cxx_standard={cxxversion}",
953                *targets,
954            )
955
956    build_bazel(
957        ctx,
958        'build',
959        '--config=stm32f429i_freertos',
960        '--//pw_thread_freertos:config_override=//pw_build:test_module_config',
961        '//pw_build:module_config_test',
962    )
963
964    # Build upstream Pigweed for the rp2040.
965    # First using the config.
966    build_bazel(
967        ctx,
968        'build',
969        '--config=rp2040',
970        '//...',
971        # Bazel will silently skip any incompatible targets in wildcard builds;
972        # but we know that some end-to-end targets definitely should remain
973        # compatible with this platform. So we list them explicitly. (If an
974        # explicitly listed target is incompatible with the platform, Bazel
975        # will return an error instead of skipping it.)
976        '//pw_system:system_example',
977    )
978    # Then using the transition.
979    #
980    # This ensures that the rp2040_binary rule transition includes all required
981    # backends.
982    build_bazel(
983        ctx,
984        'build',
985        '//pw_system:rp2040_system_example',
986    )
987
988    # Build upstream Pigweed for the Discovery board using STM32Cube.
989    build_bazel(
990        ctx,
991        'build',
992        '--config=stm32f429i_freertos',
993        '//...',
994        # Bazel will silently skip any incompatible targets in wildcard builds;
995        # but we know that some end-to-end targets definitely should remain
996        # compatible with this platform. So we list them explicitly. (If an
997        # explicitly listed target is incompatible with the platform, Bazel
998        # will return an error instead of skipping it.)
999        '//pw_system:system_example',
1000    )
1001
1002    # Build upstream Pigweed for the Discovery board using the baremetal
1003    # backends.
1004    build_bazel(
1005        ctx,
1006        'build',
1007        '--config=stm32f429i_baremetal',
1008        '//...',
1009    )
1010
1011    # Build the fuzztest example.
1012    #
1013    # TODO: b/324652164 - This doesn't work on MacOS yet.
1014    if sys.platform != 'darwin':
1015        build_bazel(
1016            ctx,
1017            'build',
1018            '--config=fuzztest',
1019            '//pw_fuzzer/examples/fuzztest:metrics_fuzztest',
1020        )
1021
1022
1023def pw_transfer_integration_test(ctx: PresubmitContext) -> None:
1024    """Runs the pw_transfer cross-language integration test only.
1025
1026    This test is not part of the regular bazel build because it's slow and
1027    intended to run in CI only.
1028    """
1029    build_bazel(
1030        ctx,
1031        'test',
1032        '//pw_transfer/integration_test:cross_language_small_test',
1033        '//pw_transfer/integration_test:cross_language_medium_read_test',
1034        '//pw_transfer/integration_test:cross_language_medium_write_test',
1035        '//pw_transfer/integration_test:cross_language_large_read_test',
1036        '//pw_transfer/integration_test:cross_language_large_write_test',
1037        '//pw_transfer/integration_test:multi_transfer_test',
1038        '//pw_transfer/integration_test:expected_errors_test',
1039        '//pw_transfer/integration_test:legacy_binaries_test',
1040        '--test_output=errors',
1041    )
1042
1043
1044#
1045# General presubmit checks
1046#
1047
1048
1049def _clang_system_include_paths(lang: str) -> list[str]:
1050    """Generate default system header paths.
1051
1052    Returns the list of system include paths used by the host
1053    clang installation.
1054    """
1055    # Dump system include paths with preprocessor verbose.
1056    command = [
1057        'clang++',
1058        '-Xpreprocessor',
1059        '-v',
1060        '-x',
1061        f'{lang}',
1062        f'{os.devnull}',
1063        '-fsyntax-only',
1064    ]
1065    process = log_run(
1066        command, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
1067    )
1068
1069    # Parse the command output to retrieve system include paths.
1070    # The paths are listed one per line.
1071    output = process.stdout.decode(errors='backslashreplace')
1072    include_paths: list[str] = []
1073    for line in output.splitlines():
1074        path = line.strip()
1075        if os.path.exists(path):
1076            include_paths.append(f'-isystem{path}')
1077
1078    return include_paths
1079
1080
1081def edit_compile_commands(
1082    in_path: Path, out_path: Path, func: Callable[[str, str, str], str]
1083) -> None:
1084    """Edit the selected compile command file.
1085
1086    Calls the input callback on all triplets (file, directory, command) in
1087    the input compile commands database. The return value replaces the old
1088    compile command in the output database.
1089    """
1090    with open(in_path) as in_file:
1091        compile_commands = json.load(in_file)
1092        for item in compile_commands:
1093            item['command'] = func(
1094                item['file'], item['directory'], item['command']
1095            )
1096    with open(out_path, 'w') as out_file:
1097        json.dump(compile_commands, out_file, indent=2)
1098
1099
1100_EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
1101    # Configuration
1102    # keep-sorted: start
1103    r'MODULE.bazel.lock',
1104    r'\b49-pico.rules$',
1105    r'\bDoxyfile$',
1106    r'\bPW_PLUGINS$',
1107    r'\bconstraint.list$',
1108    r'\bconstraint_hashes_darwin.list$',
1109    r'\bconstraint_hashes_linux.list$',
1110    r'\bconstraint_hashes_windows.list$',
1111    r'\bpython_base_requirements.txt$',
1112    r'\bupstream_requirements_darwin_lock.txt$',
1113    r'\bupstream_requirements_linux_lock.txt$',
1114    r'\bupstream_requirements_windows_lock.txt$',
1115    r'^(?:.+/)?\.bazelversion$',
1116    r'^pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version',
1117    # keep-sorted: end
1118    # Metadata
1119    # keep-sorted: start
1120    r'\b.*OWNERS.*$',
1121    r'\bAUTHORS$',
1122    r'\bLICENSE$',
1123    r'\bPIGWEED_MODULES$',
1124    r'\b\.vscodeignore$',
1125    r'\bgo.(mod|sum)$',
1126    r'\bpackage-lock.json$',
1127    r'\bpackage.json$',
1128    r'\brequirements.txt$',
1129    r'\byarn.lock$',
1130    r'^docker/tag$',
1131    r'^patches.json$',
1132    # keep-sorted: end
1133    # Data files
1134    # keep-sorted: start
1135    r'\.bin$',
1136    r'\.csv$',
1137    r'\.elf$',
1138    r'\.gif$',
1139    r'\.ico$',
1140    r'\.jpg$',
1141    r'\.json$',
1142    r'\.png$',
1143    r'\.svg$',
1144    r'\.vsix$',
1145    r'\.woff2',
1146    r'\.xml$',
1147    # keep-sorted: end
1148    # Documentation
1149    # keep-sorted: start
1150    r'\.md$',
1151    r'\.rst$',
1152    # keep-sorted: end
1153    # Generated protobuf files
1154    # keep-sorted: start
1155    r'\.pb\.c$',
1156    r'\.pb\.h$',
1157    r'\_pb2.pyi?$',
1158    # keep-sorted: end
1159    # Generated third-party files
1160    # keep-sorted: start
1161    r'\bthird_party/.*\.bazelrc$',
1162    r'\bthird_party/fuchsia/repo',
1163    r'\bthird_party/perfetto/repo/protos/perfetto/trace/perfetto_trace.proto',
1164    # keep-sorted: end
1165    # Diff/Patch files
1166    # keep-sorted: start
1167    r'\.diff$',
1168    r'\.patch$',
1169    # keep-sorted: end
1170    # Test data
1171    # keep-sorted: start
1172    r'\bpw_build/test_data/pw_copy_and_patch_file/',
1173    r'\bpw_presubmit/py/test/owners_checks/',
1174    # keep-sorted: end
1175)
1176
1177# Regular expression for the copyright comment. "\1" refers to the comment
1178# characters and "\2" refers to space after the comment characters, if any.
1179# All period characters are escaped using a replace call.
1180# pylint: disable=line-too-long
1181_COPYRIGHT = re.compile(
1182    r"""(#|//|::| \*|)( ?)Copyright 2\d{3} The Pigweed Authors
1183\1
1184\1\2Licensed under the Apache License, Version 2.0 \(the "License"\); you may not
1185\1\2use this file except in compliance with the License. You may obtain a copy of
1186\1\2the License at
1187\1
1188\1(?:\2    |\t)https://www.apache.org/licenses/LICENSE-2.0
1189\1
1190\1\2Unless required by applicable law or agreed to in writing, software
1191\1\2distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1192\1\2WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1193\1\2License for the specific language governing permissions and limitations under
1194\1\2the License.
1195""".replace(
1196        '.', r'\.'
1197    ),
1198    re.MULTILINE,
1199)
1200# pylint: enable=line-too-long
1201
1202_SKIP_LINE_PREFIXES = (
1203    '#!',
1204    '#autoload',
1205    '#compdef',
1206    '@echo off',
1207    ':<<',
1208    '/*',
1209    ' * @jest-environment jsdom',
1210    ' */',
1211    '{#',  # Jinja comment block
1212    '# -*- coding: utf-8 -*-',
1213    '<!--',
1214)
1215
1216
1217def _read_notice_lines(file: TextIO) -> Iterable[str]:
1218    lines = iter(file)
1219    try:
1220        # Read until the first line of the copyright notice.
1221        line = next(lines)
1222        while line.isspace() or line.startswith(_SKIP_LINE_PREFIXES):
1223            line = next(lines)
1224
1225        yield line
1226
1227        for _ in range(12):  # The notice is 13 lines; read the remaining 12.
1228            yield next(lines)
1229    except StopIteration:
1230        return
1231
1232
1233@filter_paths(exclude=_EXCLUDE_FROM_COPYRIGHT_NOTICE)
1234def copyright_notice(ctx: PresubmitContext):
1235    """Checks that the Pigweed copyright notice is present."""
1236    errors = []
1237
1238    for path in ctx.paths:
1239        if path.stat().st_size == 0:
1240            continue  # Skip empty files
1241
1242        try:
1243            with path.open() as file:
1244                if not _COPYRIGHT.match(''.join(_read_notice_lines(file))):
1245                    errors.append(path)
1246        except UnicodeDecodeError as exc:
1247            raise PresubmitFailure(f'failed to read {path}') from exc
1248
1249    if errors:
1250        _LOG.warning(
1251            '%s with a missing or incorrect copyright notice:\n%s',
1252            plural(errors, 'file'),
1253            '\n'.join(str(e) for e in errors),
1254        )
1255        raise PresubmitFailure
1256
1257
1258@filter_paths(endswith=format_code.CPP_SOURCE_EXTS)
1259def source_is_in_cmake_build_warn_only(ctx: PresubmitContext):
1260    """Checks that source files are in the CMake build."""
1261
1262    _run_cmake(ctx)
1263    missing = SOURCE_FILES_FILTER_CMAKE_EXCLUDE.filter(
1264        build.check_compile_commands_for_files(
1265            ctx.output_dir / 'compile_commands.json',
1266            (f for f in ctx.paths if f.suffix in format_code.CPP_SOURCE_EXTS),
1267        )
1268    )
1269    if missing:
1270        _LOG.warning(
1271            'Files missing from CMake:\n%s',
1272            '\n'.join(str(f) for f in missing),
1273        )
1274
1275
1276def build_env_setup(ctx: PresubmitContext):
1277    if 'PW_CARGO_SETUP' not in os.environ:
1278        _LOG.warning('Skipping build_env_setup since PW_CARGO_SETUP is not set')
1279        return
1280
1281    tmpl = ctx.root.joinpath('pw_env_setup', 'py', 'pyoxidizer.bzl.tmpl')
1282    out = ctx.output_dir.joinpath('pyoxidizer.bzl')
1283
1284    with open(tmpl, 'r') as ins:
1285        cfg = ins.read().replace('${PW_ROOT}', str(ctx.root))
1286        with open(out, 'w') as outs:
1287            outs.write(cfg)
1288
1289    call('pyoxidizer', 'build', cwd=ctx.output_dir)
1290
1291
1292def _valid_capitalization(word: str) -> bool:
1293    """Checks that the word has a capital letter or is not a regular word."""
1294    return bool(
1295        any(c.isupper() for c in word)  # Any capitalizatian (iTelephone)
1296        or not word.isalpha()  # Non-alphabetical (cool_stuff.exe)
1297        or shutil.which(word)
1298    )  # Matches an executable (clangd)
1299
1300
1301def commit_message_format(ctx: PresubmitContext):
1302    """Checks that the top commit's message is correctly formatted."""
1303    if git_repo.commit_author().endswith('gserviceaccount.com'):
1304        return
1305
1306    lines = git_repo.commit_message().splitlines()
1307
1308    # Ignore fixup/squash commits, but only if running locally.
1309    if not ctx.luci and lines[0].startswith(('fixup!', 'squash!')):
1310        return
1311
1312    # Show limits and current commit message in log.
1313    _LOG.debug('%-25s%+25s%+22s', 'Line limits', '72|', '72|')
1314    for line in lines:
1315        _LOG.debug(line)
1316
1317    if not lines:
1318        _LOG.error('The commit message is too short!')
1319        raise PresubmitFailure
1320
1321    # Ignore merges.
1322    repo = git_repo.LoggingGitRepo(Path.cwd())
1323    parents = repo.commit_parents()
1324    _LOG.debug('parents: %r', parents)
1325    if len(parents) > 1:
1326        _LOG.warning('Ignoring multi-parent commit')
1327        return
1328
1329    # Ignore Gerrit-generated reverts.
1330    if (
1331        'Revert' in lines[0]
1332        and 'This reverts commit ' in git_repo.commit_message()
1333        and 'Reason for revert: ' in git_repo.commit_message()
1334    ):
1335        _LOG.warning('Ignoring apparent Gerrit-generated revert')
1336        return
1337
1338    # Ignore Gerrit-generated relands
1339    if (
1340        'Reland' in lines[0]
1341        and 'This is a reland of ' in git_repo.commit_message()
1342        and "Original change's description:" in git_repo.commit_message()
1343    ):
1344        _LOG.warning('Ignoring apparent Gerrit-generated reland')
1345        return
1346
1347    errors = 0
1348
1349    if len(lines[0]) > 72:
1350        _LOG.warning(
1351            "The commit message's first line must be no longer than "
1352            '72 characters.'
1353        )
1354        _LOG.warning(
1355            'The first line is %d characters:\n  %s', len(lines[0]), lines[0]
1356        )
1357        errors += 1
1358
1359    if lines[0].endswith('.'):
1360        _LOG.warning(
1361            "The commit message's first line must not end with a period:\n %s",
1362            lines[0],
1363        )
1364        errors += 1
1365
1366    # Check that the first line matches the expected pattern.
1367    match = re.match(
1368        r'^(?P<prefix>[.\w*/]+(?:{[\w* ,]+})?[\w*/]*|SEED-\d+|clang-\w+): '
1369        r'(?P<desc>.+)$',
1370        lines[0],
1371    )
1372    if not match:
1373        _LOG.warning('The first line does not match the expected format')
1374        _LOG.warning(
1375            'Expected:\n\n  module_or_target: The description\n\n'
1376            'Found:\n\n  %s\n',
1377            lines[0],
1378        )
1379        errors += 1
1380    elif match.group('prefix') == 'roll':
1381        # We're much more flexible with roll commits.
1382        pass
1383    elif not _valid_capitalization(match.group('desc').split()[0]):
1384        _LOG.warning(
1385            'The first word after the ":" in the first line ("%s") must be '
1386            'capitalized:\n  %s',
1387            match.group('desc').split()[0],
1388            lines[0],
1389        )
1390        errors += 1
1391
1392    if len(lines) > 1 and lines[1]:
1393        _LOG.warning("The commit message's second line must be blank.")
1394        _LOG.warning(
1395            'The second line has %d characters:\n  %s', len(lines[1]), lines[1]
1396        )
1397        errors += 1
1398
1399    # Ignore the line length check for Copybara imports so they can include the
1400    # commit hash and description for imported commits.
1401    if not errors and (
1402        'Copybara import' in lines[0]
1403        and 'GitOrigin-RevId:' in git_repo.commit_message()
1404    ):
1405        _LOG.warning('Ignoring Copybara import')
1406        return
1407
1408    # Check that the lines are 72 characters or less.
1409    for i, line in enumerate(lines[2:], 3):
1410        # Skip any lines that might possibly have a URL, path, or metadata in
1411        # them.
1412        if any(c in line for c in ':/>'):
1413            continue
1414
1415        # Skip any lines with non-ASCII characters.
1416        if not line.isascii():
1417            continue
1418
1419        # Skip any blockquoted lines.
1420        if line.startswith('  '):
1421            continue
1422
1423        if len(line) > 72:
1424            _LOG.warning(
1425                'Commit message lines must be no longer than 72 characters.'
1426            )
1427            _LOG.warning('Line %d has %d characters:\n  %s', i, len(line), line)
1428            errors += 1
1429
1430    if errors:
1431        _LOG.error('Found %s in the commit message', plural(errors, 'error'))
1432        raise PresubmitFailure
1433
1434
1435@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.py'))
1436def static_analysis(ctx: PresubmitContext):
1437    """Runs all available static analysis tools."""
1438    build.gn_gen(ctx)
1439    build.ninja(ctx, 'python.lint', 'static_analysis')
1440    build.gn_check(ctx)
1441
1442
1443_EXCLUDE_FROM_TODO_CHECK = (
1444    # keep-sorted: start
1445    r'.bazelrc$',
1446    r'.dockerignore$',
1447    r'.gitignore$',
1448    r'.pylintrc$',
1449    r'.ruff.toml$',
1450    r'MODULE.bazel.lock$',
1451    r'\bdocs/build_system.rst',
1452    r'\bdocs/code_reviews.rst',
1453    r'\bpw_assert_basic/basic_handler.cc',
1454    r'\bpw_assert_basic/public/pw_assert_basic/handler.h',
1455    r'\bpw_blob_store/public/pw_blob_store/flat_file_system_entry.h',
1456    r'\bpw_build/linker_script.gni',
1457    r'\bpw_build/py/pw_build/copy_from_cipd.py',
1458    r'\bpw_cpu_exception/basic_handler.cc',
1459    r'\bpw_cpu_exception_cortex_m/entry.cc',
1460    r'\bpw_cpu_exception_cortex_m/exception_entry_test.cc',
1461    r'\bpw_doctor/py/pw_doctor/doctor.py',
1462    r'\bpw_env_setup/util.sh',
1463    r'\bpw_fuzzer/fuzzer.gni',
1464    r'\bpw_i2c/BUILD.gn',
1465    r'\bpw_i2c/public/pw_i2c/register_device.h',
1466    r'\bpw_kvs/flash_memory.cc',
1467    r'\bpw_kvs/key_value_store.cc',
1468    r'\bpw_log_basic/log_basic.cc',
1469    r'\bpw_package/py/pw_package/packages/chromium_verifier.py',
1470    r'\bpw_protobuf/encoder.cc',
1471    r'\bpw_rpc/docs.rst',
1472    r'\bpw_watch/py/pw_watch/watch.py',
1473    r'\btargets/mimxrt595_evk/BUILD.bazel',
1474    r'\btargets/stm32f429i_disc1/boot.cc',
1475    r'\bthird_party/chromium_verifier/BUILD.gn',
1476    # keep-sorted: end
1477)
1478
1479
1480@filter_paths(exclude=_EXCLUDE_FROM_TODO_CHECK)
1481def todo_check_with_exceptions(ctx: PresubmitContext):
1482    """Check that non-legacy TODO lines are valid."""  # todo-check: ignore
1483    todo_check.create(todo_check.BUGS_OR_USERNAMES)(ctx)
1484
1485
1486@filter_paths(file_filter=format_code.OWNERS_CODE_FORMAT.filter)
1487def owners_lint_checks(ctx: PresubmitContext):
1488    """Runs OWNERS linter."""
1489    owners_checks.presubmit_check(ctx.paths)
1490
1491
1492SOURCE_FILES_FILTER = FileFilter(
1493    endswith=_BUILD_FILE_FILTER.endswith,
1494    suffix=('.bazel', '.bzl', '.gn', '.gni', *_BUILD_FILE_FILTER.suffix),
1495    exclude=(
1496        r'zephyr.*',
1497        r'android.*',
1498        r'\.black.toml',
1499        r'pyproject.toml',
1500    ),
1501)
1502
1503SOURCE_FILES_FILTER_GN_EXCLUDE = FileFilter(
1504    exclude=(
1505        # keep-sorted: start
1506        r'\bpw_bluetooth_sapphire/fuchsia',
1507        # keep-sorted: end
1508    ),
1509)
1510
1511SOURCE_FILES_FILTER_CMAKE_EXCLUDE = FileFilter(
1512    exclude=(
1513        # keep-sorted: start
1514        r'\bpw_bluetooth_sapphire/fuchsia',
1515        # keep-sorted: end
1516    ),
1517)
1518
1519# cc_library targets which contain the forbidden `includes` attribute.
1520#
1521# TODO: https://pwbug.dev/378564135 - Burn this list down.
1522INCLUDE_CHECK_EXCEPTIONS = (
1523    # keep-sorted: start
1524    "//pw_allocator/block:alignable",
1525    "//pw_allocator/block:allocatable",
1526    "//pw_allocator/block:basic",
1527    "//pw_allocator/block:contiguous",
1528    "//pw_allocator/block:detailed_block",
1529    "//pw_allocator/block:iterable",
1530    "//pw_allocator/block:poisonable",
1531    "//pw_allocator/block:result",
1532    "//pw_allocator/block:testing",
1533    "//pw_allocator/block:with_layout",
1534    "//pw_allocator/bucket:base",
1535    "//pw_allocator/bucket:fast_sorted",
1536    "//pw_allocator/bucket:sequenced",
1537    "//pw_allocator/bucket:sorted",
1538    "//pw_allocator/bucket:testing",
1539    "//pw_allocator/bucket:unordered",
1540    "//pw_allocator/examples:custom_allocator",
1541    "//pw_allocator/examples:custom_allocator_test_harness",
1542    "//pw_allocator/examples:named_u32",
1543    "//pw_allocator:best_fit_block_allocator",
1544    "//pw_allocator:first_fit_block_allocator",
1545    "//pw_allocator:worst_fit_block_allocator",
1546    "//pw_assert:assert.facade",
1547    "//pw_assert:assert_compatibility_backend",
1548    "//pw_assert:check.facade",
1549    "//pw_assert:libc_assert",
1550    "//pw_assert:print_and_abort_assert_backend",
1551    "//pw_assert:print_and_abort_check_backend",
1552    "//pw_assert_basic:handler.facade",
1553    "//pw_assert_basic:pw_assert_basic",
1554    "//pw_assert_fuchsia:pw_assert_fuchsia",
1555    "//pw_assert_log:assert_backend",
1556    "//pw_assert_log:check_and_assert_backend",
1557    "//pw_assert_log:check_backend",
1558    "//pw_assert_tokenized:pw_assert_tokenized",
1559    "//pw_assert_trap:pw_assert_trap",
1560    "//pw_async2_basic:dispatcher",
1561    "//pw_async2_epoll:dispatcher",
1562    "//pw_async:fake_dispatcher.facade",
1563    "//pw_async:task.facade",
1564    "//pw_async_basic:fake_dispatcher",
1565    "//pw_async_basic:task",
1566    "//pw_async_fuchsia:dispatcher",
1567    "//pw_async_fuchsia:fake_dispatcher",
1568    "//pw_async_fuchsia:task",
1569    "//pw_async_fuchsia:util",
1570    "//pw_bluetooth:emboss_att",
1571    "//pw_bluetooth:emboss_hci_android",
1572    "//pw_bluetooth:emboss_hci_commands",
1573    "//pw_bluetooth:emboss_hci_common",
1574    "//pw_bluetooth:emboss_hci_data",
1575    "//pw_bluetooth:emboss_hci_events",
1576    "//pw_bluetooth:emboss_hci_h4",
1577    "//pw_bluetooth:emboss_hci_test",
1578    "//pw_bluetooth:emboss_l2cap_frames",
1579    "//pw_bluetooth:emboss_rfcomm_frames",
1580    "//pw_bluetooth:emboss_util",
1581    "//pw_bluetooth:pw_bluetooth",
1582    "//pw_bluetooth:pw_bluetooth2",
1583    "//pw_boot:pw_boot.facade",
1584    "//pw_build/bazel_internal:header_test",
1585    "//pw_chrono:system_clock.facade",
1586    "//pw_chrono:system_timer.facade",
1587    "//pw_chrono_embos:system_clock",
1588    "//pw_chrono_embos:system_timer",
1589    "//pw_chrono_freertos:system_clock",
1590    "//pw_chrono_freertos:system_timer",
1591    "//pw_chrono_rp2040:system_clock",
1592    "//pw_chrono_stl:system_clock",
1593    "//pw_chrono_stl:system_timer",
1594    "//pw_chrono_threadx:system_clock",
1595    "//pw_cpu_exception:entry.facade",
1596    "//pw_cpu_exception:handler.facade",
1597    "//pw_cpu_exception:support.facade",
1598    "//pw_cpu_exception_cortex_m:cpu_exception",
1599    "//pw_cpu_exception_cortex_m:crash.facade",
1600    "//pw_cpu_exception_cortex_m:crash_test.lib",
1601    "//pw_crypto:ecdsa.facade",
1602    "//pw_crypto:sha256.facade",
1603    "//pw_crypto:sha256_mbedtls",
1604    "//pw_crypto:sha256_mock",
1605    "//pw_fuzzer/examples/fuzztest:metrics_lib",
1606    "//pw_fuzzer:fuzztest",
1607    "//pw_fuzzer:fuzztest_stub",
1608    "//pw_grpc:connection",
1609    "//pw_grpc:grpc_channel_output",
1610    "//pw_grpc:pw_rpc_handler",
1611    "//pw_grpc:send_queue",
1612    "//pw_interrupt:context.facade",
1613    "//pw_interrupt_cortex_m:context",
1614    "//pw_log:pw_log.facade",
1615    "//pw_log_basic:headers",
1616    "//pw_log_fuchsia:pw_log_fuchsia",
1617    "//pw_log_null:headers",
1618    "//pw_log_string:handler.facade",
1619    "//pw_log_string:pw_log_string",
1620    "//pw_log_tokenized:gcc_partially_tokenized",
1621    "//pw_log_tokenized:handler.facade",
1622    "//pw_log_tokenized:pw_log_tokenized",
1623    "//pw_malloc:pw_malloc.facade",
1624    "//pw_metric:metric_service_pwpb",
1625    "//pw_multibuf:internal_test_utils",
1626    "//pw_perf_test:arm_cortex_timer",
1627    "//pw_perf_test:chrono_timer",
1628    "//pw_perf_test:timer.facade",
1629    "//pw_polyfill:standard_library",
1630    "//pw_rpc:internal_test_utils",
1631    "//pw_sensor:pw_sensor_types",
1632    "//pw_sync:binary_semaphore.facade",
1633    "//pw_sync:binary_semaphore_thread_notification_backend",
1634    "//pw_sync:binary_semaphore_timed_thread_notification_backend",
1635    "//pw_sync:counting_semaphore.facade",
1636    "//pw_sync:interrupt_spin_lock.facade",
1637    "//pw_sync:mutex.facade",
1638    "//pw_sync:recursive_mutex.facade",
1639    "//pw_sync:thread_notification.facade",
1640    "//pw_sync:timed_mutex.facade",
1641    "//pw_sync:timed_thread_notification.facade",
1642    "//pw_sync_baremetal:interrupt_spin_lock",
1643    "//pw_sync_baremetal:mutex",
1644    "//pw_sync_baremetal:recursive_mutex",
1645    "//pw_sync_embos:binary_semaphore",
1646    "//pw_sync_embos:counting_semaphore",
1647    "//pw_sync_embos:interrupt_spin_lock",
1648    "//pw_sync_embos:mutex",
1649    "//pw_sync_embos:timed_mutex",
1650    "//pw_sync_freertos:binary_semaphore",
1651    "//pw_sync_freertos:counting_semaphore",
1652    "//pw_sync_freertos:interrupt_spin_lock",
1653    "//pw_sync_freertos:mutex",
1654    "//pw_sync_freertos:thread_notification",
1655    "//pw_sync_freertos:timed_mutex",
1656    "//pw_sync_freertos:timed_thread_notification",
1657    "//pw_sync_stl:binary_semaphore",
1658    "//pw_sync_stl:condition_variable",
1659    "//pw_sync_stl:counting_semaphore",
1660    "//pw_sync_stl:interrupt_spin_lock",
1661    "//pw_sync_stl:mutex",
1662    "//pw_sync_stl:recursive_mutex",
1663    "//pw_sync_stl:timed_mutex",
1664    "//pw_sync_threadx:binary_semaphore",
1665    "//pw_sync_threadx:counting_semaphore",
1666    "//pw_sync_threadx:interrupt_spin_lock",
1667    "//pw_sync_threadx:mutex",
1668    "//pw_sync_threadx:timed_mutex",
1669    "//pw_sys_io:pw_sys_io.facade",
1670    "//pw_system:device_handler.facade",
1671    "//pw_system:io.facade",
1672    "//pw_thread:id.facade",
1673    "//pw_thread:sleep.facade",
1674    "//pw_thread:test_thread_context.facade",
1675    "//pw_thread:thread.facade",
1676    "//pw_thread:thread_iteration.facade",
1677    "//pw_thread:yield.facade",
1678    "//pw_thread_embos:id",
1679    "//pw_thread_embos:sleep",
1680    "//pw_thread_embos:thread",
1681    "//pw_thread_embos:yield",
1682    "//pw_thread_freertos:freertos_tasktcb",
1683    "//pw_thread_freertos:id",
1684    "//pw_thread_freertos:sleep",
1685    "//pw_thread_freertos:test_thread_context",
1686    "//pw_thread_freertos:thread",
1687    "//pw_thread_freertos:yield",
1688    "//pw_thread_stl:id",
1689    "//pw_thread_stl:sleep",
1690    "//pw_thread_stl:test_thread_context",
1691    "//pw_thread_stl:thread",
1692    "//pw_thread_stl:yield",
1693    "//pw_thread_threadx:id",
1694    "//pw_thread_threadx:sleep",
1695    "//pw_thread_threadx:thread",
1696    "//pw_thread_threadx:yield",
1697    "//pw_tls_client:entropy.facade",
1698    "//pw_tls_client:pw_tls_client.facade",
1699    "//pw_tls_client_boringssl:pw_tls_client_boringssl",
1700    "//pw_tls_client_mbedtls:pw_tls_client_mbedtls",
1701    "//pw_trace:null",
1702    "//pw_trace:pw_trace.facade",
1703    "//pw_trace:pw_trace_sample_app",
1704    "//pw_trace:trace_facade_test.lib",
1705    "//pw_trace:trace_zero_facade_test.lib",
1706    "//pw_trace_tokenized:pw_trace_example_to_file",
1707    "//pw_trace_tokenized:pw_trace_host_trace_time",
1708    "//pw_trace_tokenized:pw_trace_tokenized",
1709    "//pw_trace_tokenized:trace_tokenized_test.lib",
1710    "//pw_unit_test:googletest",
1711    "//pw_unit_test:light",
1712    "//pw_unit_test:pw_unit_test.facade",
1713    "//pw_unit_test:rpc_service",
1714    "//targets/mimxrt595_evk_freertos:freertos_config",
1715    "//targets/rp2040:freertos_config",
1716    "//targets/stm32f429i_disc1_stm32cube:freertos_config",
1717    "//targets/stm32f429i_disc1_stm32cube:hal_config",
1718    "//third_party/boringssl:sysdeps",
1719    "//third_party/chromium_verifier:pthread",
1720    "//third_party/fuchsia:fit_impl",
1721    "//third_party/fuchsia:stdcompat",
1722    "//third_party/mbedtls:default_config",
1723    "//third_party/smartfusion_mss:debug_config",
1724    "//third_party/smartfusion_mss:default_config",
1725    # keep-sorted: end
1726)
1727
1728INCLUDE_CHECK_TARGET_PATTERN = "//... " + " ".join(
1729    "-" + target for target in INCLUDE_CHECK_EXCEPTIONS
1730)
1731
1732#
1733# Presubmit check programs
1734#
1735
1736OTHER_CHECKS = (
1737    # keep-sorted: start
1738    bazel_test,
1739    bthost_package,
1740    build.gn_gen_check,
1741    cmake_clang,
1742    cmake_gcc,
1743    coverage,
1744    # TODO: b/234876100 - Remove once msan is added to all_sanitizers().
1745    cpp_checks.msan,
1746    docs_build,
1747    gitmodules.create(gitmodules.Config(allow_submodules=False)),
1748    gn_all,
1749    gn_clang_build,
1750    gn_combined_build_check,
1751    gn_main_build_check,
1752    gn_platform_build_check,
1753    module_owners.presubmit_check(),
1754    npm_presubmit.npm_test,
1755    pw_transfer_integration_test,
1756    python_checks.update_upstream_python_constraints,
1757    python_checks.upload_pigweed_pypi_distribution,
1758    python_checks.vendor_python_wheels,
1759    python_checks.version_bump_pigweed_pypi_distribution,
1760    shell_checks.shellcheck,
1761    # TODO(hepler): Many files are missing from the CMake build. Add this check
1762    # to lintformat when the missing files are fixed.
1763    source_in_build.cmake(SOURCE_FILES_FILTER, _run_cmake),
1764    source_in_build.soong(SOURCE_FILES_FILTER),
1765    static_analysis,
1766    stm32f429i,
1767    todo_check.create(todo_check.BUGS_OR_USERNAMES),
1768    zephyr_build,
1769    # keep-sorted: end
1770)
1771
1772ARDUINO_PICO = (
1773    # Skip gn_teensy_build if running on mac-arm64.
1774    # There are no arm specific tools packages available upstream:
1775    # https://www.pjrc.com/teensy/package_teensy_index.json
1776    gn_teensy_build
1777    if not (sys.platform == 'darwin' and platform.machine() == 'arm64')
1778    else (),
1779    gn_pico_build,
1780    gn_pw_system_demo_build,
1781)
1782
1783INTERNAL = (gn_mimxrt595_build, gn_mimxrt595_freertos_build)
1784
1785SAPPHIRE = (
1786    # keep-sorted: start
1787    gn_chre_googletest_nanopb_sapphire_build,
1788    # keep-sorted: end
1789)
1790
1791SANITIZERS = (cpp_checks.all_sanitizers(),)
1792
1793SECURITY = (
1794    # keep-sorted: start
1795    gn_crypto_mbedtls_build,
1796    gn_crypto_micro_ecc_build,
1797    gn_software_update_build,
1798    # keep-sorted: end
1799)
1800
1801FUZZ = (gn_fuzz_build, oss_fuzz_build)
1802
1803_LINTFORMAT = (
1804    bazel_checks.includes_presubmit_check(INCLUDE_CHECK_TARGET_PATTERN),
1805    commit_message_format,
1806    copyright_notice,
1807    format_code.presubmit_checks(),
1808    inclusive_language.presubmit_check.with_filter(
1809        exclude=(
1810            r'\bMODULE.bazel.lock$',
1811            r'\bgo.sum$',
1812            r'\bpackage-lock.json$',
1813            r'\byarn.lock$',
1814        )
1815    ),
1816    cpp_checks.pragma_once,
1817    build.bazel_lint,
1818    owners_lint_checks,
1819    source_in_build.gn(SOURCE_FILES_FILTER).with_file_filter(
1820        SOURCE_FILES_FILTER_GN_EXCLUDE
1821    ),
1822    source_is_in_cmake_build_warn_only,
1823    javascript_checks.eslint if shutil.which('npm') else (),
1824    json_check.presubmit_check,
1825    keep_sorted.presubmit_check,
1826    todo_check_with_exceptions,
1827)
1828
1829LINTFORMAT = (
1830    _LINTFORMAT,
1831    # This check is excluded from _LINTFORMAT because it's not quick: it issues
1832    # a bazel query that pulls in all of Pigweed's external dependencies
1833    # (https://stackoverflow.com/q/71024130/1224002). These are cached, but
1834    # after a roll it can be quite slow.
1835    source_in_build.bazel(SOURCE_FILES_FILTER),
1836    python_checks.check_python_versions,
1837    python_checks.gn_python_lint,
1838)
1839
1840QUICK = (
1841    _LINTFORMAT,
1842    gn_quick_build_check,
1843)
1844
1845FULL = (
1846    _LINTFORMAT,
1847    gn_combined_build_check,
1848    gn_host_tools,
1849    bazel_test,
1850    bazel_build,
1851    python_checks.gn_python_check,
1852    python_checks.gn_python_test_coverage,
1853    python_checks.check_upstream_python_constraints,
1854    build_env_setup,
1855)
1856
1857PROGRAMS = Programs(
1858    # keep-sorted: start
1859    arduino_pico=ARDUINO_PICO,
1860    full=FULL,
1861    fuzz=FUZZ,
1862    internal=INTERNAL,
1863    lintformat=LINTFORMAT,
1864    other_checks=OTHER_CHECKS,
1865    quick=QUICK,
1866    sanitizers=SANITIZERS,
1867    sapphire=SAPPHIRE,
1868    security=SECURITY,
1869    # keep-sorted: end
1870)
1871
1872
1873def parse_args() -> argparse.Namespace:
1874    """Creates an argument parser and parses arguments."""
1875
1876    parser = argparse.ArgumentParser(description=__doc__)
1877    cli.add_arguments(parser, PROGRAMS, 'quick')
1878    parser.add_argument(
1879        '--install',
1880        action='store_true',
1881        help='Install the presubmit as a Git pre-push hook and exit.',
1882    )
1883
1884    return parser.parse_args()
1885
1886
1887def run(install: bool, **presubmit_args) -> int:
1888    """Entry point for presubmit."""
1889
1890    if install:
1891        install_git_hook(
1892            'pre-push',
1893            [
1894                'python',
1895                '-m',
1896                'pw_presubmit.pigweed_presubmit',
1897                '--base',
1898                'origin/main..HEAD',
1899                '--program',
1900                'quick',
1901            ],
1902        )
1903        return 0
1904
1905    return cli.run(**presubmit_args)
1906
1907
1908def main() -> int:
1909    """Run the presubmit for the Pigweed repository."""
1910    return run(**vars(parse_args()))
1911
1912
1913if __name__ == '__main__':
1914    try:
1915        # If pw_cli is available, use it to initialize logs.
1916        from pw_cli import log  # pylint: disable=ungrouped-imports
1917
1918        log.install(logging.INFO)
1919    except ImportError:
1920        # If pw_cli isn't available, display log messages like a simple print.
1921        logging.basicConfig(format='%(message)s', level=logging.INFO)
1922
1923    sys.exit(main())
1924