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