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