• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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