1#!/usr/bin/env python3 2# Copyright 2022 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import os 7import typing 8from typing import Generator, List, Literal, Optional, Tuple, Union 9 10from impl.common import ( 11 CROSVM_ROOT, 12 TOOLS_ROOT, 13 Triple, 14 argh, 15 chdir, 16 cmd, 17 run_main, 18) 19from impl.presubmit import Check, CheckContext, run_checks, Group 20 21python = cmd("python3") 22mypy = cmd("mypy").with_color_env("MYPY_FORCE_COLOR") 23black = cmd("black").with_color_arg(always="--color", never="--no-color") 24mdformat = cmd("mdformat") 25lucicfg = cmd("third_party/depot_tools/lucicfg") 26 27# All supported platforms as a type and a list. 28Platform = Literal["x86_64", "aarch64", "mingw64", "armhf"] 29PLATFORMS: Tuple[Platform, ...] = typing.get_args(Platform) 30 31ClippyOnlyPlatform = Literal["android"] 32CLIPPY_ONLY_PLATFORMS: Tuple[ClippyOnlyPlatform, ...] = typing.get_args(ClippyOnlyPlatform) 33 34 35def platform_is_supported(platform: Union[Platform, ClippyOnlyPlatform]): 36 "Returns true if the platform is available as a target in rustup." 37 triple = Triple.from_shorthand(platform) 38 installed_toolchains = cmd("rustup target list --installed").lines() 39 return str(triple) in installed_toolchains 40 41 42#################################################################################################### 43# Check methods 44# 45# Each check returns a Command (or list of Commands) to be run to execute the check. They are 46# registered and configured in the CHECKS list below. 47# 48# Some check functions are factory functions that return a check command for all supported 49# platforms. 50 51 52def check_python_tests(_: CheckContext): 53 "Runs unit tests for python dev tooling." 54 PYTHON_TESTS = [ 55 # Disabled due to b/309148074 56 # "tests.cl_tests", 57 "impl.common", 58 ] 59 return [python.with_cwd(TOOLS_ROOT).with_args("-m", file) for file in PYTHON_TESTS] 60 61 62def check_python_types(context: CheckContext): 63 "Run mypy type checks on python dev tooling." 64 return [mypy("--pretty", file) for file in context.all_files] 65 66 67def check_python_format(context: CheckContext): 68 "Runs the black formatter on python dev tooling." 69 return black.with_args( 70 "--check" if not context.fix else None, 71 *context.modified_files, 72 ) 73 74 75def check_markdown_format(context: CheckContext): 76 "Runs mdformat on all markdown files." 77 if "blaze" in mdformat("--version").stdout(): 78 raise Exception( 79 "You are using google's mdformat. " 80 + "Please update your PATH to ensure the pip installed mdformat is available." 81 ) 82 return mdformat.with_args( 83 "--wrap 100", 84 "--check" if not context.fix else "", 85 *context.modified_files, 86 ) 87 88 89def check_rust_format(context: CheckContext): 90 "Runs rustfmt on all modified files." 91 rustfmt = cmd(cmd("rustup +nightly which rustfmt").stdout()) 92 # Windows doesn't accept very long arguments: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa#:~:text=The%20maximum%20length%20of%20this%20string%20is%2032%2C767%20characters%2C%20including%20the%20Unicode%20terminating%20null%20character.%20If%20lpApplicationName%20is%20NULL%2C%20the%20module%20name%20portion%20of%20lpCommandLine%20is%20limited%20to%20MAX_PATH%20characters. 93 return list( 94 rustfmt.with_color_flag() 95 .with_args("--check" if not context.fix else "") 96 .foreach(context.modified_files, batch_size=10) 97 ) 98 99 100def check_cargo_doc(_: CheckContext): 101 "Runs cargo-doc and verifies that no warnings are emitted." 102 return cmd("./tools/cargo-doc").with_env("RUSTDOCFLAGS", "-D warnings").with_color_flag() 103 104 105def check_doc_tests(_: CheckContext): 106 "Runs cargo doc tests. These cannot be run via nextest and run_tests." 107 return cmd( 108 "cargo test", 109 "--doc", 110 "--workspace", 111 "--features=all-x86_64", 112 ).with_color_flag() 113 114 115def check_mdbook(_: CheckContext): 116 "Runs cargo-doc and verifies that no warnings are emitted." 117 return cmd("mdbook build docs/book/") 118 119 120def check_crosvm_tests(platform: Platform): 121 def check(_: CheckContext): 122 if not platform_is_supported(platform): 123 return None 124 dut = None 125 if os.access("/dev/kvm", os.W_OK): 126 if platform == "x86_64": 127 dut = "--dut=vm" 128 elif platform == "aarch64": 129 dut = "--dut=vm" 130 return cmd("./tools/run_tests --verbose --platform", platform, dut).with_color_flag() 131 132 check.__doc__ = f"Runs all crosvm tests for {platform}." 133 134 return check 135 136 137def check_crosvm_unit_tests(platform: Platform): 138 def check(_: CheckContext): 139 if not platform_is_supported(platform): 140 return None 141 return cmd("./tools/run_tests --verbose --platform", platform).with_color_flag() 142 143 check.__doc__ = f"Runs crosvm unit tests for {platform}." 144 145 return check 146 147 148def check_crosvm_build( 149 platform: Platform, features: Optional[str] = None, no_default_features: bool = False 150): 151 def check(_: CheckContext): 152 return cmd( 153 "./tools/run_tests --no-run --verbose --platform", 154 platform, 155 f"--features={features}" if features is not None else None, 156 "--no-default-features" if no_default_features else None, 157 ).with_color_flag() 158 159 check.__doc__ = f"Builds crosvm for {platform} with features {features}." 160 161 return check 162 163 164def check_clippy(platform: Union[Platform, ClippyOnlyPlatform]): 165 def check(context: CheckContext): 166 if not platform_is_supported(platform): 167 return None 168 return cmd( 169 "./tools/clippy --platform", 170 platform, 171 "--fix" if context.fix else None, 172 ).with_color_flag() 173 174 check.__doc__ = f"Runs clippy for {platform}." 175 176 return check 177 178 179def custom_check(name: str, can_fix: bool = False): 180 "Custom checks are written in python in tools/custom_checks. This is a wrapper to call them." 181 182 def check(context: CheckContext): 183 return cmd( 184 TOOLS_ROOT / "custom_checks", 185 name, 186 *context.modified_files, 187 "--fix" if can_fix and context.fix else None, 188 ) 189 190 check.__name__ = name.replace("-", "_") 191 check.__doc__ = f"Runs tools/custom_check {name}" 192 return check 193 194 195#################################################################################################### 196# Checks configuration 197# 198# Configures which checks are available and on which files they are run. 199# Check names default to the function name minus the check_ prefix 200 201CHECKS: List[Check] = [ 202 Check( 203 check_rust_format, 204 files=["**.rs"], 205 exclude=["system_api/src/bindings/*"], 206 can_fix=True, 207 ), 208 Check( 209 check_mdbook, 210 files=["docs/**/*"], 211 ), 212 Check( 213 check_cargo_doc, 214 files=["**.rs", "**Cargo.toml"], 215 priority=True, 216 ), 217 Check( 218 check_doc_tests, 219 files=["**.rs", "**Cargo.toml"], 220 priority=True, 221 ), 222 Check( 223 check_python_tests, 224 files=["tools/**.py"], 225 python_tools=True, 226 priority=True, 227 ), 228 Check( 229 check_python_types, 230 files=["tools/**.py"], 231 exclude=[ 232 "tools/windows/*", 233 "tools/contrib/memstats_chart/*", 234 "tools/contrib/cros_tracing_analyser/*", 235 ], 236 python_tools=True, 237 ), 238 Check( 239 check_python_format, 240 files=["**.py"], 241 python_tools=True, 242 exclude=["infra/recipes.py"], 243 can_fix=True, 244 ), 245 Check( 246 check_markdown_format, 247 files=["**.md"], 248 exclude=[ 249 "infra/README.recipes.md", 250 "docs/book/src/appendix/memory_layout.md", 251 ], 252 can_fix=True, 253 ), 254 *( 255 Check( 256 check_crosvm_build(platform, features="default"), 257 custom_name=f"crosvm_build_default_{platform}", 258 files=["**.rs"], 259 priority=True, 260 ) 261 for platform in PLATFORMS 262 ), 263 *( 264 Check( 265 check_crosvm_build(platform, features="", no_default_features=True), 266 custom_name=f"crosvm_build_no_default_{platform}", 267 files=["**.rs"], 268 priority=True, 269 ) 270 # TODO: b/260607247 crosvm does not compile with no-default-features on mingw64 271 for platform in PLATFORMS 272 if platform != "mingw64" 273 ), 274 *( 275 Check( 276 check_crosvm_tests(platform), 277 custom_name=f"crosvm_tests_{platform}", 278 files=["**.rs"], 279 priority=True, 280 ) 281 for platform in PLATFORMS 282 ), 283 *( 284 Check( 285 check_crosvm_unit_tests(platform), 286 custom_name=f"crosvm_unit_tests_{platform}", 287 files=["**.rs"], 288 priority=True, 289 ) 290 for platform in PLATFORMS 291 ), 292 *( 293 Check( 294 check_clippy(platform), 295 custom_name=f"clippy_{platform}", 296 files=["**.rs"], 297 can_fix=True, 298 priority=True, 299 ) 300 for platform in (*PLATFORMS, *CLIPPY_ONLY_PLATFORMS) 301 ), 302 Check( 303 custom_check("check-copyright-header"), 304 files=["**.rs", "**.py", "**.c", "**.h", "**.policy", "**.sh"], 305 exclude=[ 306 "infra/recipes.py", 307 "hypervisor/src/whpx/whpx_sys/*.h", 308 "third_party/vmm_vhost/*", 309 "net_sys/src/lib.rs", 310 "system_api/src/bindings/*", 311 ], 312 python_tools=True, 313 can_fix=True, 314 ), 315 Check( 316 custom_check("check-rust-features"), 317 files=["**Cargo.toml"], 318 ), 319 Check( 320 custom_check("check-rust-lockfiles"), 321 files=["**Cargo.toml"], 322 ), 323 Check( 324 custom_check("check-line-endings"), 325 ), 326 Check( 327 custom_check("check-file-ends-with-newline"), 328 exclude=[ 329 "**.h264", 330 "**.vp8", 331 "**.vp9", 332 "**.ivf", 333 "**.bin", 334 "**.png", 335 "**.min.js", 336 "**.drawio", 337 "**.json", 338 "**.dtb", 339 "**.dtbo", 340 ], 341 ), 342] 343 344#################################################################################################### 345# Group configuration 346# 347# Configures pre-defined groups of checks. Some are configured for CI builders and others 348# are configured for convenience during local development. 349 350GROUPS: List[Group] = [ 351 # The default group is run if no check or group is explicitly set 352 Group( 353 name="default", 354 doc="Checks run by default", 355 checks=[ 356 "default_health_checks", 357 # Run only one task per platform to prevent blocking on the build cache. 358 "crosvm_tests_x86_64", 359 "crosvm_unit_tests_aarch64", 360 "crosvm_unit_tests_mingw64", 361 "clippy_armhf", 362 ], 363 ), 364 Group( 365 name="quick", 366 doc="Runs a quick subset of presubmit checks.", 367 checks=[ 368 "default_health_checks", 369 "crosvm_unit_tests_x86_64", 370 "clippy_aarch64", 371 ], 372 ), 373 Group( 374 name="all", 375 doc="Run checks of all builders.", 376 checks=[ 377 "health_checks", 378 *(f"linux_{platform}" for platform in PLATFORMS), 379 *(f"clippy_{platform}" for platform in CLIPPY_ONLY_PLATFORMS), 380 ], 381 ), 382 # Convenience groups for local usage: 383 Group( 384 name="clippy", 385 doc="Runs clippy for all platforms", 386 checks=[f"clippy_{platform}" for platform in PLATFORMS + CLIPPY_ONLY_PLATFORMS], 387 ), 388 Group( 389 name="unit_tests", 390 doc="Runs unit tests for all platforms", 391 checks=[f"crosvm_unit_tests_{platform}" for platform in PLATFORMS], 392 ), 393 Group( 394 name="format", 395 doc="Runs all formatting checks (or fixes)", 396 checks=[ 397 "rust_format", 398 "markdown_format", 399 "python_format", 400 ], 401 ), 402 Group( 403 name="default_health_checks", 404 doc="Health checks to run by default", 405 checks=[ 406 # Check if lockfiles need updating first. Otherwise another step may do the update. 407 "rust_lockfiles", 408 "copyright_header", 409 "file_ends_with_newline", 410 "line_endings", 411 "markdown_format", 412 "mdbook", 413 "cargo_doc", 414 "python_format", 415 "python_types", 416 "rust_features", 417 "rust_format", 418 ], 419 ), 420 # The groups below are used by builders in CI: 421 Group( 422 name="health_checks", 423 doc="Checks run on the health_check builder", 424 checks=[ 425 "default_health_checks", 426 "doc_tests", 427 "python_tests", 428 ], 429 ), 430 Group( 431 name="android-aarch64", 432 doc="Checks run on the android-aarch64 builder", 433 checks=[ 434 "clippy_android", 435 ], 436 ), 437 *( 438 Group( 439 name=f"linux_{platform}", 440 doc=f"Checks run on the linux-{platform} builder", 441 checks=[ 442 f"crosvm_tests_{platform}", 443 f"clippy_{platform}", 444 f"crosvm_build_default_{platform}", 445 ] 446 # TODO: b/260607247 crosvm does not compile with no-default-features on mingw64 447 + ([f"crosvm_build_no_default_{platform}"] if platform != "mingw64" else []), 448 ) 449 for platform in PLATFORMS 450 ), 451] 452 453# Turn both lists into dicts for convenience 454CHECKS_DICT = dict((c.name, c) for c in CHECKS) 455GROUPS_DICT = dict((c.name, c) for c in GROUPS) 456 457 458def validate_config(): 459 "Validates the CHECKS and GROUPS configuration." 460 for group in GROUPS: 461 for check in group.checks: 462 if check not in CHECKS_DICT and check not in GROUPS_DICT: 463 raise Exception(f"Group {group.name} includes non-existing item {check}.") 464 465 def find_in_group(check: Check): 466 for group in GROUPS: 467 if check.name in group.checks: 468 return True 469 return False 470 471 for check in CHECKS: 472 if not find_in_group(check): 473 raise Exception(f"Check {check.name} is not included in any group.") 474 475 all_names = [c.name for c in CHECKS] + [g.name for g in GROUPS] 476 for name in all_names: 477 if all_names.count(name) > 1: 478 raise Exception(f"Check or group {name} is defined multiple times.") 479 480 481def get_check_names_in_group(group: Group) -> Generator[str, None, None]: 482 for name in group.checks: 483 if name in GROUPS_DICT: 484 yield from get_check_names_in_group(GROUPS_DICT[name]) 485 else: 486 yield name 487 488 489@argh.arg("--list-checks", default=False, help="List names of available checks and exit.") 490@argh.arg("--fix", default=False, help="Asks checks to fix problems where possible.") 491@argh.arg("--no-delta", default=False, help="Run on all files instead of just modified files.") 492@argh.arg("--no-parallel", default=False, help="Do not run checks in parallel.") 493@argh.arg( 494 "checks_or_groups", 495 help="List of checks or groups to run. Defaults to run the `default` group.", 496) 497def main( 498 list_checks: bool = False, 499 fix: bool = False, 500 no_delta: bool = False, 501 no_parallel: bool = False, 502 *checks_or_groups: str, 503): 504 chdir(CROSVM_ROOT) 505 validate_config() 506 507 if not checks_or_groups: 508 checks_or_groups = ("default",) 509 510 # Resolve and validate the groups and checks provided 511 check_names: List[str] = [] 512 for check_or_group in checks_or_groups: 513 if check_or_group in CHECKS_DICT: 514 check_names.append(check_or_group) 515 elif check_or_group in GROUPS_DICT: 516 check_names += list(get_check_names_in_group(GROUPS_DICT[check_or_group])) 517 else: 518 raise Exception(f"No such check or group: {check_or_group}") 519 520 # Remove duplicates while preserving order 521 check_names = list(dict.fromkeys(check_names)) 522 523 if list_checks: 524 for check in check_names: 525 print(check) 526 return 527 528 check_list = [CHECKS_DICT[name] for name in check_names] 529 530 run_checks( 531 check_list, 532 fix=fix, 533 run_on_all_files=no_delta, 534 parallel=not no_parallel, 535 ) 536 537 538def usage(): 539 groups = "\n".join(f" {group.name}: {group.doc}" for group in GROUPS) 540 checks = "\n".join(f" {check.name}: {check.doc}" for check in CHECKS) 541 return f"""\ 542Runs checks on the crosvm codebase. 543 544Basic usage, to run a default selection of checks: 545 546 ./tools/presubmit 547 548Some checkers can fix issues they find (e.g. formatters, clippy, etc): 549 550 ./tools/presubmit --fix 551 552 553Various groups of presubmit checks can be run via: 554 555 ./tools/presubmit group_name 556 557Available groups are: 558{groups} 559 560You can also provide the names of specific checks to run: 561 562 ./tools/presubmit check1 check2 563 564Available checks are: 565{checks} 566""" 567 568 569if __name__ == "__main__": 570 run_main(main, usage=usage()) 571