123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- #!/usr/bin/env python3
- # Copyright 2022 The ChromiumOS Authors
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- import os
- import typing
- from typing import Generator, List, Literal, Optional, Tuple, Union
- from impl.common import (
- CROSVM_ROOT,
- TOOLS_ROOT,
- Triple,
- argh,
- chdir,
- cmd,
- run_main,
- )
- from impl.presubmit import Check, CheckContext, run_checks, Group
- python = cmd("python3")
- mypy = cmd("mypy").with_color_env("MYPY_FORCE_COLOR")
- black = cmd("black").with_color_arg(always="--color", never="--no-color")
- mdformat = cmd("mdformat")
- lucicfg = cmd("third_party/depot_tools/lucicfg")
- # All supported platforms as a type and a list.
- Platform = Literal["x86_64", "aarch64", "mingw64", "armhf"]
- PLATFORMS: Tuple[Platform, ...] = typing.get_args(Platform)
- ClippyOnlyPlatform = Literal["android"]
- CLIPPY_ONLY_PLATFORMS: Tuple[ClippyOnlyPlatform, ...] = typing.get_args(ClippyOnlyPlatform)
- def platform_is_supported(platform: Union[Platform, ClippyOnlyPlatform]):
- "Returns true if the platform is available as a target in rustup."
- triple = Triple.from_shorthand(platform)
- installed_toolchains = cmd("rustup target list --installed").lines()
- return str(triple) in installed_toolchains
- ####################################################################################################
- # Check methods
- #
- # Each check returns a Command (or list of Commands) to be run to execute the check. They are
- # registered and configured in the CHECKS list below.
- #
- # Some check functions are factory functions that return a check command for all supported
- # platforms.
- def check_python_tests(_: CheckContext):
- "Runs unit tests for python dev tooling."
- PYTHON_TESTS = [
- # Disabled due to b/309148074
- # "tests.cl_tests",
- "impl.common",
- ]
- return [python.with_cwd(TOOLS_ROOT).with_args("-m", file) for file in PYTHON_TESTS]
- def check_python_types(context: CheckContext):
- "Run mypy type checks on python dev tooling."
- return [mypy("--pretty", file) for file in context.all_files]
- def check_python_format(context: CheckContext):
- "Runs the black formatter on python dev tooling."
- return black.with_args(
- "--check" if not context.fix else None,
- *context.modified_files,
- )
- def check_markdown_format(context: CheckContext):
- "Runs mdformat on all markdown files."
- if "blaze" in mdformat("--version").stdout():
- raise Exception(
- "You are using google's mdformat. "
- + "Please update your PATH to ensure the pip installed mdformat is available."
- )
- return mdformat.with_args(
- "--wrap 100",
- "--check" if not context.fix else "",
- *context.modified_files,
- )
- def check_rust_format(context: CheckContext):
- "Runs rustfmt on all modified files."
- rustfmt = cmd(cmd("rustup +nightly which rustfmt").stdout())
- # 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.
- return list(
- rustfmt.with_color_flag()
- .with_args("--check" if not context.fix else "")
- .foreach(context.modified_files, batch_size=10)
- )
- def check_cargo_doc(_: CheckContext):
- "Runs cargo-doc and verifies that no warnings are emitted."
- return cmd("./tools/cargo-doc").with_env("RUSTDOCFLAGS", "-D warnings").with_color_flag()
- def check_doc_tests(_: CheckContext):
- "Runs cargo doc tests. These cannot be run via nextest and run_tests."
- return cmd(
- "cargo test",
- "--doc",
- "--workspace",
- "--features=all-x86_64",
- ).with_color_flag()
- def check_mdbook(_: CheckContext):
- "Runs cargo-doc and verifies that no warnings are emitted."
- return cmd("mdbook build docs/book/")
- def check_crosvm_tests(platform: Platform):
- def check(_: CheckContext):
- if not platform_is_supported(platform):
- return None
- dut = None
- if os.access("/dev/kvm", os.W_OK):
- if platform == "x86_64":
- dut = "--dut=vm"
- elif platform == "aarch64":
- dut = "--dut=vm"
- return cmd("./tools/run_tests --verbose --platform", platform, dut).with_color_flag()
- check.__doc__ = f"Runs all crosvm tests for {platform}."
- return check
- def check_crosvm_unit_tests(platform: Platform):
- def check(_: CheckContext):
- if not platform_is_supported(platform):
- return None
- return cmd("./tools/run_tests --verbose --platform", platform).with_color_flag()
- check.__doc__ = f"Runs crosvm unit tests for {platform}."
- return check
- def check_crosvm_build(
- platform: Platform, features: Optional[str] = None, no_default_features: bool = False
- ):
- def check(_: CheckContext):
- return cmd(
- "./tools/run_tests --no-run --verbose --platform",
- platform,
- f"--features={features}" if features is not None else None,
- "--no-default-features" if no_default_features else None,
- ).with_color_flag()
- check.__doc__ = f"Builds crosvm for {platform} with features {features}."
- return check
- def check_clippy(platform: Union[Platform, ClippyOnlyPlatform]):
- def check(context: CheckContext):
- if not platform_is_supported(platform):
- return None
- return cmd(
- "./tools/clippy --platform",
- platform,
- "--fix" if context.fix else None,
- ).with_color_flag()
- check.__doc__ = f"Runs clippy for {platform}."
- return check
- def custom_check(name: str, can_fix: bool = False):
- "Custom checks are written in python in tools/custom_checks. This is a wrapper to call them."
- def check(context: CheckContext):
- return cmd(
- TOOLS_ROOT / "custom_checks",
- name,
- *context.modified_files,
- "--fix" if can_fix and context.fix else None,
- )
- check.__name__ = name.replace("-", "_")
- check.__doc__ = f"Runs tools/custom_check {name}"
- return check
- ####################################################################################################
- # Checks configuration
- #
- # Configures which checks are available and on which files they are run.
- # Check names default to the function name minus the check_ prefix
- CHECKS: List[Check] = [
- Check(
- check_rust_format,
- files=["**.rs"],
- exclude=["system_api/src/bindings/*"],
- can_fix=True,
- ),
- Check(
- check_mdbook,
- files=["docs/**/*"],
- ),
- Check(
- check_cargo_doc,
- files=["**.rs", "**Cargo.toml"],
- priority=True,
- ),
- Check(
- check_doc_tests,
- files=["**.rs", "**Cargo.toml"],
- priority=True,
- ),
- Check(
- check_python_tests,
- files=["tools/**.py"],
- python_tools=True,
- priority=True,
- ),
- Check(
- check_python_types,
- files=["tools/**.py"],
- exclude=[
- "tools/windows/*",
- "tools/contrib/memstats_chart/*",
- "tools/contrib/cros_tracing_analyser/*",
- ],
- python_tools=True,
- ),
- Check(
- check_python_format,
- files=["**.py"],
- python_tools=True,
- exclude=["infra/recipes.py"],
- can_fix=True,
- ),
- Check(
- check_markdown_format,
- files=["**.md"],
- exclude=[
- "infra/README.recipes.md",
- "docs/book/src/appendix/memory_layout.md",
- ],
- can_fix=True,
- ),
- *(
- Check(
- check_crosvm_build(platform, features="default"),
- custom_name=f"crosvm_build_default_{platform}",
- files=["**.rs"],
- priority=True,
- )
- for platform in PLATFORMS
- ),
- *(
- Check(
- check_crosvm_build(platform, features="", no_default_features=True),
- custom_name=f"crosvm_build_no_default_{platform}",
- files=["**.rs"],
- priority=True,
- )
- # TODO: b/260607247 crosvm does not compile with no-default-features on mingw64
- for platform in PLATFORMS
- if platform != "mingw64"
- ),
- *(
- Check(
- check_crosvm_tests(platform),
- custom_name=f"crosvm_tests_{platform}",
- files=["**.rs"],
- priority=True,
- )
- for platform in PLATFORMS
- ),
- *(
- Check(
- check_crosvm_unit_tests(platform),
- custom_name=f"crosvm_unit_tests_{platform}",
- files=["**.rs"],
- priority=True,
- )
- for platform in PLATFORMS
- ),
- *(
- Check(
- check_clippy(platform),
- custom_name=f"clippy_{platform}",
- files=["**.rs"],
- can_fix=True,
- priority=True,
- )
- for platform in (*PLATFORMS, *CLIPPY_ONLY_PLATFORMS)
- ),
- Check(
- custom_check("check-copyright-header"),
- files=["**.rs", "**.py", "**.c", "**.h", "**.policy", "**.sh"],
- exclude=[
- "infra/recipes.py",
- "hypervisor/src/whpx/whpx_sys/*.h",
- "third_party/vmm_vhost/*",
- "net_sys/src/lib.rs",
- "system_api/src/bindings/*",
- ],
- python_tools=True,
- can_fix=True,
- ),
- Check(
- custom_check("check-rust-features"),
- files=["**Cargo.toml"],
- ),
- Check(
- custom_check("check-rust-lockfiles"),
- files=["**Cargo.toml"],
- ),
- Check(
- custom_check("check-line-endings"),
- ),
- Check(
- custom_check("check-file-ends-with-newline"),
- exclude=[
- "**.h264",
- "**.vp8",
- "**.vp9",
- "**.ivf",
- "**.bin",
- "**.png",
- "**.min.js",
- "**.drawio",
- "**.json",
- "**.dtb",
- "**.dtbo",
- ],
- ),
- ]
- ####################################################################################################
- # Group configuration
- #
- # Configures pre-defined groups of checks. Some are configured for CI builders and others
- # are configured for convenience during local development.
- GROUPS: List[Group] = [
- # The default group is run if no check or group is explicitly set
- Group(
- name="default",
- doc="Checks run by default",
- checks=[
- "default_health_checks",
- # Run only one task per platform to prevent blocking on the build cache.
- "crosvm_tests_x86_64",
- "crosvm_unit_tests_aarch64",
- "crosvm_unit_tests_mingw64",
- "clippy_armhf",
- ],
- ),
- Group(
- name="quick",
- doc="Runs a quick subset of presubmit checks.",
- checks=[
- "default_health_checks",
- "crosvm_unit_tests_x86_64",
- "clippy_aarch64",
- ],
- ),
- Group(
- name="all",
- doc="Run checks of all builders.",
- checks=[
- "health_checks",
- *(f"linux_{platform}" for platform in PLATFORMS),
- *(f"clippy_{platform}" for platform in CLIPPY_ONLY_PLATFORMS),
- ],
- ),
- # Convenience groups for local usage:
- Group(
- name="clippy",
- doc="Runs clippy for all platforms",
- checks=[f"clippy_{platform}" for platform in PLATFORMS + CLIPPY_ONLY_PLATFORMS],
- ),
- Group(
- name="unit_tests",
- doc="Runs unit tests for all platforms",
- checks=[f"crosvm_unit_tests_{platform}" for platform in PLATFORMS],
- ),
- Group(
- name="format",
- doc="Runs all formatting checks (or fixes)",
- checks=[
- "rust_format",
- "markdown_format",
- "python_format",
- ],
- ),
- Group(
- name="default_health_checks",
- doc="Health checks to run by default",
- checks=[
- # Check if lockfiles need updating first. Otherwise another step may do the update.
- "rust_lockfiles",
- "copyright_header",
- "file_ends_with_newline",
- "line_endings",
- "markdown_format",
- "mdbook",
- "cargo_doc",
- "python_format",
- "python_types",
- "rust_features",
- "rust_format",
- ],
- ),
- # The groups below are used by builders in CI:
- Group(
- name="health_checks",
- doc="Checks run on the health_check builder",
- checks=[
- "default_health_checks",
- "doc_tests",
- "python_tests",
- ],
- ),
- Group(
- name="android-aarch64",
- doc="Checks run on the android-aarch64 builder",
- checks=[
- "clippy_android",
- ],
- ),
- *(
- Group(
- name=f"linux_{platform}",
- doc=f"Checks run on the linux-{platform} builder",
- checks=[
- f"crosvm_tests_{platform}",
- f"clippy_{platform}",
- f"crosvm_build_default_{platform}",
- ]
- # TODO: b/260607247 crosvm does not compile with no-default-features on mingw64
- + ([f"crosvm_build_no_default_{platform}"] if platform != "mingw64" else []),
- )
- for platform in PLATFORMS
- ),
- ]
- # Turn both lists into dicts for convenience
- CHECKS_DICT = dict((c.name, c) for c in CHECKS)
- GROUPS_DICT = dict((c.name, c) for c in GROUPS)
- def validate_config():
- "Validates the CHECKS and GROUPS configuration."
- for group in GROUPS:
- for check in group.checks:
- if check not in CHECKS_DICT and check not in GROUPS_DICT:
- raise Exception(f"Group {group.name} includes non-existing item {check}.")
- def find_in_group(check: Check):
- for group in GROUPS:
- if check.name in group.checks:
- return True
- return False
- for check in CHECKS:
- if not find_in_group(check):
- raise Exception(f"Check {check.name} is not included in any group.")
- all_names = [c.name for c in CHECKS] + [g.name for g in GROUPS]
- for name in all_names:
- if all_names.count(name) > 1:
- raise Exception(f"Check or group {name} is defined multiple times.")
- def get_check_names_in_group(group: Group) -> Generator[str, None, None]:
- for name in group.checks:
- if name in GROUPS_DICT:
- yield from get_check_names_in_group(GROUPS_DICT[name])
- else:
- yield name
- @argh.arg("--list-checks", default=False, help="List names of available checks and exit.")
- @argh.arg("--fix", default=False, help="Asks checks to fix problems where possible.")
- @argh.arg("--no-delta", default=False, help="Run on all files instead of just modified files.")
- @argh.arg("--no-parallel", default=False, help="Do not run checks in parallel.")
- @argh.arg(
- "checks_or_groups",
- help="List of checks or groups to run. Defaults to run the `default` group.",
- )
- def main(
- list_checks: bool = False,
- fix: bool = False,
- no_delta: bool = False,
- no_parallel: bool = False,
- *checks_or_groups: str,
- ):
- chdir(CROSVM_ROOT)
- validate_config()
- if not checks_or_groups:
- checks_or_groups = ("default",)
- # Resolve and validate the groups and checks provided
- check_names: List[str] = []
- for check_or_group in checks_or_groups:
- if check_or_group in CHECKS_DICT:
- check_names.append(check_or_group)
- elif check_or_group in GROUPS_DICT:
- check_names += list(get_check_names_in_group(GROUPS_DICT[check_or_group]))
- else:
- raise Exception(f"No such check or group: {check_or_group}")
- # Remove duplicates while preserving order
- check_names = list(dict.fromkeys(check_names))
- if list_checks:
- for check in check_names:
- print(check)
- return
- check_list = [CHECKS_DICT[name] for name in check_names]
- run_checks(
- check_list,
- fix=fix,
- run_on_all_files=no_delta,
- parallel=not no_parallel,
- )
- def usage():
- groups = "\n".join(f" {group.name}: {group.doc}" for group in GROUPS)
- checks = "\n".join(f" {check.name}: {check.doc}" for check in CHECKS)
- return f"""\
- Runs checks on the crosvm codebase.
- Basic usage, to run a default selection of checks:
- ./tools/presubmit
- Some checkers can fix issues they find (e.g. formatters, clippy, etc):
- ./tools/presubmit --fix
- Various groups of presubmit checks can be run via:
- ./tools/presubmit group_name
- Available groups are:
- {groups}
- You can also provide the names of specific checks to run:
- ./tools/presubmit check1 check2
- Available checks are:
- {checks}
- """
- if __name__ == "__main__":
- run_main(main, usage=usage())
|