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 typing 7from typing import Generator, List, Literal, Optional, Tuple 8 9from impl.common import ( 10 CROSVM_ROOT, 11 TOOLS_ROOT, 12 Triple, 13 argh, 14 chdir, 15 cmd, 16 is_kiwi_repo, 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 "tests.cl_tests", 53 "impl.common", 54 ] 55 return [python.with_cwd(TOOLS_ROOT).with_args("-m", file) for file in PYTHON_TESTS] 56 57 58def check_python_types(context: CheckContext): 59 "Run mypy type checks on python dev tooling." 60 return [mypy("--pretty", file) for file in context.all_files] 61 62 63def check_python_format(context: CheckContext): 64 "Runs the black formatter on python dev tooling." 65 return black.with_args( 66 "--check" if not context.fix else None, 67 *context.modified_files, 68 ) 69 70 71def check_markdown_format(context: CheckContext): 72 "Runs mdformat on all markdown files." 73 if "blaze" in mdformat("--version").stdout(): 74 raise Exception( 75 "You are using google's mdformat. " 76 + "Please update your PATH to ensure the pip installed mdformat is available." 77 ) 78 return mdformat.with_args( 79 "--wrap 100", 80 "--check" if not context.fix else "", 81 *context.modified_files, 82 ) 83 84 85def check_rust_format(context: CheckContext): 86 "Runs rustfmt on all modified files." 87 if context.nightly_fmt: 88 rustfmt = cmd( 89 cmd("rustup +nightly which rustfmt"), 90 "--config imports_granularity=item,group_imports=StdExternalCrate", 91 ) 92 else: 93 rustfmt = cmd(cmd("rustup which rustfmt")) 94 return rustfmt.with_color_flag().with_args( 95 "--check" if not context.fix else "", 96 *context.modified_files, 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_mdbook(_: CheckContext): 106 "Runs cargo-doc and verifies that no warnings are emitted." 107 return cmd("mdbook build docs/book/") 108 109 110def check_crosvm_tests(platform: Platform): 111 def check(_: CheckContext): 112 if not platform_is_supported(platform): 113 return None 114 dut = None 115 if platform == "x86_64": 116 dut = "--dut=host" 117 elif platform == "aarch64": 118 dut = "--dut=vm" 119 return cmd("./tools/run_tests --platform", platform, dut).with_color_flag() 120 121 check.__doc__ = f"Runs all crosvm tests for {platform}." 122 123 return check 124 125 126def check_crosvm_unit_tests(platform: Platform): 127 def check(_: CheckContext): 128 if not platform_is_supported(platform): 129 return None 130 return cmd("./tools/run_tests --platform", platform).with_color_flag() 131 132 check.__doc__ = f"Runs crosvm unit tests for {platform}." 133 134 return check 135 136 137def check_crosvm_build(platform: Platform, features: Optional[str]): 138 def check(_: CheckContext): 139 return cmd( 140 "./tools/run_tests --no-run --platform", 141 platform, 142 f"--features={features}" if features is not None else None, 143 ).with_color_flag() 144 145 check.__doc__ = f"Builds crosvm for {platform} with features {features}." 146 147 return check 148 149 150def check_clippy(platform: Platform): 151 def check(context: CheckContext): 152 if not platform_is_supported(platform): 153 return None 154 return cmd( 155 "./tools/clippy --platform", 156 platform, 157 "--fix" if context.fix else None, 158 ).with_color_flag() 159 160 check.__doc__ = f"Runs clippy for {platform}." 161 162 return check 163 164 165def check_infra_configs(context: CheckContext): 166 "Validate luci configs by sending them to luci-config." 167 # TODO: Validate config files. Requires authentication with luci inside docker. 168 return [lucicfg("fmt --dry-run", file) for file in context.modified_files] 169 170 171def check_infra_tests(_: CheckContext): 172 "Run recipe.py tests, all of them, regardless of which files were modified." 173 recipes = cmd("infra/recipes.py").with_path_env("third_party/depot_tools") 174 return recipes("test run") 175 176 177def custom_check(name: str, can_fix: bool = False): 178 "Custom checks are written in python in tools/custom_checks. This is a wrapper to call them." 179 180 def check(context: CheckContext): 181 return cmd( 182 TOOLS_ROOT / "custom_checks", 183 name, 184 *context.modified_files, 185 "--fix" if can_fix and context.fix else None, 186 ) 187 188 check.__name__ = name.replace("-", "_") 189 check.__doc__ = f"Runs tools/custom_check {name}" 190 return check 191 192 193#################################################################################################### 194# Checks configuration 195# 196# Configures which checks are available and on which files they are run. 197# Check names default to the function name minus the check_ prefix 198 199CHECKS: List[Check] = [ 200 Check( 201 check_rust_format, 202 files=["**.rs"], 203 exclude=["system_api/src/bindings/*"], 204 can_fix=True, 205 ), 206 Check( 207 check_mdbook, 208 files=["docs/**/*"], 209 ), 210 Check( 211 check_cargo_doc, 212 files=["**.rs", "**Cargo.toml"], 213 priority=True, 214 ), 215 Check( 216 check_python_tests, 217 files=["tools/**.py"], 218 python_tools=True, 219 priority=True, 220 ), 221 Check( 222 check_python_types, 223 files=["tools/**.py"], 224 exclude=["tools/windows/*"], 225 python_tools=True, 226 ), 227 Check( 228 check_python_format, 229 files=["**.py"], 230 python_tools=True, 231 exclude=["infra/recipes.py"], 232 can_fix=True, 233 ), 234 Check( 235 check_markdown_format, 236 files=["**.md"], 237 exclude=[ 238 "infra/README.recipes.md", 239 "docs/book/src/appendix/memory_layout.md", 240 ], 241 can_fix=True, 242 ), 243 Check( 244 check_crosvm_build("x86_64", features="direct"), 245 custom_name=f"crosvm_build_direct", 246 files=["**.rs"], 247 priority=True, 248 ), 249 *( 250 Check( 251 check_crosvm_build(platform, features="default"), 252 custom_name=f"crosvm_build_default_{platform}", 253 files=["**.rs"], 254 priority=True, 255 ) 256 for platform in PLATFORMS 257 ), 258 *( 259 Check( 260 check_crosvm_tests(platform), 261 custom_name=f"crosvm_tests_{platform}", 262 files=["**.rs"], 263 priority=True, 264 ) 265 for platform in PLATFORMS 266 ), 267 *( 268 Check( 269 check_crosvm_unit_tests(platform), 270 custom_name=f"crosvm_unit_tests_{platform}", 271 files=["**.rs"], 272 priority=True, 273 ) 274 for platform in PLATFORMS 275 ), 276 *( 277 Check( 278 check_clippy(platform), 279 custom_name=f"clippy_{platform}", 280 files=["**.rs"], 281 can_fix=True, 282 priority=True, 283 ) 284 for platform in PLATFORMS 285 ), 286 Check( 287 custom_check("check-copyright-header"), 288 files=["**.rs", "**.py", "**.c", "**.h", "**.policy", "**.sh"], 289 exclude=[ 290 "infra/recipes.py", 291 "hypervisor/src/whpx/whpx_sys/*.h", 292 "third_party/vmm_vhost/*", 293 "net_sys/src/lib.rs", 294 "system_api/src/bindings/*", 295 ], 296 python_tools=True, 297 can_fix=True, 298 ), 299 Check( 300 custom_check("check-rust-features"), 301 files=["**Cargo.toml"], 302 ), 303 Check( 304 custom_check("check-rust-lockfiles"), 305 files=["**Cargo.toml"], 306 ), 307 Check( 308 custom_check("check-line-endings"), 309 ), 310 Check( 311 custom_check("check-file-ends-with-newline"), 312 exclude=[ 313 "**.h264", 314 "**.vp8", 315 "**.vp9", 316 "**.ivf", 317 "**.bin", 318 "**.png", 319 "**.min.js", 320 "**.drawio", 321 "**.json", 322 ], 323 ), 324] 325 326# We disable LUCI infra related tests because kokoro doesn't have internet connectivity that 327# the tests rely on. 328if not is_kiwi_repo(): 329 CHECKS.extend( 330 [ 331 Check( 332 check_infra_configs, 333 files=["infra/config/**.star"], 334 can_fix=True, 335 ), 336 Check( 337 check_infra_tests, 338 files=["infra/**.py"], 339 can_fix=True, 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 "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 "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 "linux_x86_64_direct", 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], 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 # The groups below are used by builders in CI: 403 Group( 404 name="health_checks", 405 doc="Checks run on the health_check builder", 406 checks=[ 407 "copyright_header", 408 "file_ends_with_newline", 409 "infra_configs", 410 "infra_tests", 411 "line_endings", 412 "markdown_format", 413 "mdbook", 414 "cargo_doc", 415 "python_format", 416 "python_tests", 417 "python_types", 418 "rust_features", 419 "rust_format", 420 "rust_lockfiles", 421 ], 422 ), 423 *( 424 Group( 425 name=f"linux_{platform}", 426 doc=f"Checks run on the linux-{platform} builder", 427 checks=[ 428 f"crosvm_tests_{platform}", 429 f"clippy_{platform}", 430 f"crosvm_build_default_{platform}", 431 ], 432 ) 433 for platform in PLATFORMS 434 ), 435 Group( 436 name=f"linux_x86_64_direct", 437 doc=f"Checks run on the linux_x86_64_direct builder", 438 checks=[ 439 "crosvm_build_direct", 440 ], 441 ), 442] 443 444# Turn both lists into dicts for convenience 445CHECKS_DICT = dict((c.name, c) for c in CHECKS) 446GROUPS_DICT = dict((c.name, c) for c in GROUPS) 447 448 449def validate_config(): 450 "Validates the CHECKS and GROUPS configuration." 451 for group in GROUPS: 452 for check in group.checks: 453 if check not in CHECKS_DICT and check not in GROUPS_DICT: 454 raise Exception(f"Group {group.name} includes non-existing item {check}.") 455 456 def find_in_group(check: Check): 457 for group in GROUPS: 458 if check.name in group.checks: 459 return True 460 return False 461 462 for check in CHECKS: 463 if not find_in_group(check): 464 raise Exception(f"Check {check.name} is not included in any group.") 465 466 all_names = [c.name for c in CHECKS] + [g.name for g in GROUPS] 467 for name in all_names: 468 if all_names.count(name) > 1: 469 raise Exception(f"Check or group {name} is defined multiple times.") 470 471 472def get_check_names_in_group(group: Group) -> Generator[str, None, None]: 473 for name in group.checks: 474 if name in GROUPS_DICT: 475 yield from get_check_names_in_group(GROUPS_DICT[name]) 476 else: 477 yield name 478 479 480@argh.arg("--list-checks", default=False, help="List names of available checks and exit.") 481@argh.arg("--fix", default=False, help="Asks checks to fix problems where possible.") 482@argh.arg("--no-delta", default=False, help="Run on all files instead of just modified files.") 483@argh.arg("--no-parallel", default=False, help="Do not run checks in parallel.") 484@argh.arg("--nightly-fmt", default=False, help="Use nightly rust for rustfmt") 485@argh.arg( 486 "checks_or_groups", 487 help="List of checks or groups to run. Defaults to run the `default` group.", 488) 489def main( 490 list_checks: bool = False, 491 fix: bool = False, 492 no_delta: bool = False, 493 no_parallel: bool = False, 494 nightly_fmt: bool = False, 495 *checks_or_groups: str, 496): 497 chdir(CROSVM_ROOT) 498 validate_config() 499 500 if not checks_or_groups: 501 checks_or_groups = ("default",) 502 503 # Resolve and validate the groups and checks provided 504 check_names: List[str] = [] 505 for check_or_group in checks_or_groups: 506 if check_or_group in CHECKS_DICT: 507 check_names.append(check_or_group) 508 elif check_or_group in GROUPS_DICT: 509 check_names += list(get_check_names_in_group(GROUPS_DICT[check_or_group])) 510 else: 511 raise Exception(f"No such check or group: {check_or_group}") 512 513 # Remove duplicates while preserving order 514 check_names = list(dict.fromkeys(check_names)) 515 516 if list_checks: 517 for check in check_names: 518 print(check) 519 return 520 521 check_list = [CHECKS_DICT[name] for name in check_names] 522 523 run_checks( 524 check_list, 525 fix=fix, 526 run_on_all_files=no_delta, 527 nightly_fmt=nightly_fmt, 528 parallel=not no_parallel, 529 ) 530 531 532def usage(): 533 groups = "\n".join(f" {group.name}: {group.doc}" for group in GROUPS) 534 checks = "\n".join(f" {check.name}: {check.doc}" for check in CHECKS) 535 return f"""\ 536Runs checks on the crosvm codebase. 537 538Basic usage, to run a default selection of checks: 539 540 ./tools/presubmit 541 542Some checkers can fix issues they find (e.g. formatters, clippy, etc): 543 544 ./tools/presubmit --fix 545 546 547Various groups of presubmit checks can be run via: 548 549 ./tools/presubmit group_name 550 551Available groups are: 552{groups} 553 554You can also provide the names of specific checks to run: 555 556 ./tools/presubmit check1 check2 557 558Available checks are: 559{checks} 560""" 561 562 563if __name__ == "__main__": 564 run_main(main, usage=usage()) 565