• 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
6# Contains custom presubmit checks implemented in python.
7#
8# These are implemented as a separate CLI tool from tools/presubmit as the presubmit
9# framework needs to call a subprocess to execute checks.
10
11from fnmatch import fnmatch
12import os
13import re
14import json
15from datetime import datetime
16from pathlib import Path
17from typing import Dict, Generator, List, cast
18
19from impl.common import (
20    cmd,
21    cwd_context,
22    run_commands,
23)
24
25
26def check_platform_independent(*files: str):
27    "Checks the provided files to ensure they are free of platform independent code."
28    cfg_unix = "cfg.*unix"
29    cfg_linux = "cfg.*linux"
30    cfg_windows = "cfg.*windows"
31    cfg_android = "cfg.*android"
32    target_os = "target_os = "
33
34    target_os_pattern = re.compile(
35        "%s|%s|%s|%s|%s" % (cfg_android, cfg_linux, cfg_unix, cfg_windows, target_os)
36    )
37
38    for file in files:
39        for line_number, line in enumerate(open(file, encoding="utf8")):
40            if re.search(target_os_pattern, line):
41                raise Exception(f"Found unexpected platform dependent code in {file}:{line_number}")
42
43
44CRLF_LINE_ENDING_FILES: List[str] = [
45    "**.bat",
46    "**.ps1",
47    "e2e_tests/tests/goldens/backcompat_test_simple_lspci_win.txt",
48    "tools/windows/build_test",
49]
50
51
52def is_crlf_file(file: str):
53    for glob in CRLF_LINE_ENDING_FILES:
54        if fnmatch(file, glob):
55            return True
56    return False
57
58
59def check_line_endings(*files: str):
60    "Checks line endings. Windows only files are using clrf. All others just lf."
61    for line in cmd("git ls-files --eol", *files).lines():
62        parts = line.split()
63        file = parts[-1]
64        index_endings = parts[0][2:]
65        wdir_endings = parts[1][2:]
66
67        def check_endings(endings: str):
68            if is_crlf_file(file):
69                if endings not in ("crlf", "mixed"):
70                    raise Exception(f"{file} Expected crlf file endings. Found {endings}")
71            else:
72                if endings in ("crlf", "mixed"):
73                    raise Exception(f"{file} Expected lf file endings. Found {endings}")
74
75        check_endings(index_endings)
76        check_endings(wdir_endings)
77
78
79def check_rust_lockfiles(*_files: str):
80    "Verifies that none of the Cargo.lock files require updates."
81    lockfiles = [Path("Cargo.lock"), *Path("common").glob("*/Cargo.lock")]
82    for path in lockfiles:
83        with cwd_context(path.parent):
84            if not cmd("cargo update --workspace --locked").success():
85                print(f"{path} is not up-to-date.")
86                print()
87                print("You may need to rebase your changes and run `cargo update --workspace`")
88                print("(or ./tools/run_tests) to ensure the Cargo.lock file is current.")
89                raise Exception("Cargo.lock out of date")
90
91
92# These crosvm features are currently not built upstream. Do not add to this list.
93KNOWN_DISABLED_FEATURES = [
94    "default-no-sandbox",
95    "libvda",
96    "seccomp_trace",
97    "vulkano",
98    "whpx",
99]
100
101
102def check_rust_features(*_files: str):
103    "Verifies that all cargo features are included in the list of features we compile upstream."
104    metadata = json.loads(cmd("cargo metadata --format-version=1").stdout())
105    crosvm_metadata = next(p for p in metadata["packages"] if p["name"] == "crosvm")
106    features = cast(Dict[str, List[str]], crosvm_metadata["features"])
107
108    def collect_features(feature_name: str) -> Generator[str, None, None]:
109        yield feature_name
110        for feature in features[feature_name]:
111            if feature in features:
112                yield from collect_features(feature)
113            else:
114                # optional crate is enabled through sub-feature of the crate.
115                # e.g. protos optional crate/feature is enabled by protos/plugin
116                optional_crate_name = feature.split("/")[0]
117                if (
118                    optional_crate_name in features
119                    and features[optional_crate_name][0] == f"dep:{optional_crate_name}"
120                ):
121                    yield optional_crate_name
122
123    all_platform_features = set(
124        (
125            *collect_features("all-x86_64"),
126            *collect_features("all-aarch64"),
127            *collect_features("all-armhf"),
128            *collect_features("all-mingw64"),
129            *collect_features("all-msvc64"),
130            *collect_features("all-riscv64"),
131        )
132    )
133    disabled_features = [
134        feature
135        for feature in features
136        if feature not in all_platform_features and feature not in KNOWN_DISABLED_FEATURES
137    ]
138    if disabled_features:
139        raise Exception(
140            f"The features {', '.join(disabled_features)} are not enabled in upstream crosvm builds."
141        )
142
143
144LICENSE_HEADER_RE = (
145    r".*Copyright (?P<year>20[0-9]{2})(?:-20[0-9]{2})? The ChromiumOS Authors\n"
146    r".*Use of this source code is governed by a BSD-style license that can be\n"
147    r".*found in the LICENSE file\.\n"
148    r"( *\*/\n)?"  # allow the end of a C-style comment before the blank line
149    r"\n"
150)
151
152NEW_LICENSE_HEADER = [
153    f"Copyright {datetime.now().year} The ChromiumOS Authors",
154    "Use of this source code is governed by a BSD-style license that can be",
155    "found in the LICENSE file.",
156]
157
158
159def new_licence_header(file_suffix: str):
160    if file_suffix in (".py", "", ".policy", ".sh"):
161        prefix = "#"
162    else:
163        prefix = "//"
164    return "\n".join(f"{prefix} {line}" for line in NEW_LICENSE_HEADER) + "\n\n"
165
166
167def check_copyright_header(*files: str, fix: bool = False):
168    "Checks copyright header. Can 'fix' them if needed by adding the header."
169    license_re = re.compile(LICENSE_HEADER_RE, re.MULTILINE)
170    for file_path in (Path(f) for f in files):
171        header = file_path.open("r").read(512)
172        license_match = license_re.search(header)
173        if license_match:
174            continue
175        # Generated files do not need a copyright header.
176        if "generated by" in header:
177            continue
178        if fix:
179            print(f"Adding copyright header: {file_path}")
180            contents = file_path.read_text()
181            file_path.write_text(new_licence_header(file_path.suffix) + contents)
182        else:
183            raise Exception(f"Bad copyright header: {file_path}")
184
185
186def check_file_ends_with_newline(*files: str, fix: bool = False):
187    "Checks if files end with a newline."
188    for file_path in (Path(f) for f in files):
189        with file_path.open("rb") as file:
190            # Skip empty files
191            file.seek(0, os.SEEK_END)
192            if file.tell() == 0:
193                continue
194            # Check last byte of the file
195            file.seek(-1, os.SEEK_END)
196            file_end = file.read(1)
197            if file_end.decode("utf-8") != "\n":
198                if fix:
199                    file_path.write_text(file_path.read_text() + "\n")
200                else:
201                    raise Exception(f"File does not end with a newline {file_path}")
202
203
204if __name__ == "__main__":
205    run_commands(
206        check_file_ends_with_newline,
207        check_copyright_header,
208        check_rust_features,
209        check_rust_lockfiles,
210        check_line_endings,
211        check_platform_independent,
212    )
213