xref: /aosp_15_r20/external/crosvm/tools/presubmit (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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