• 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]
49
50
51def is_crlf_file(file: str):
52    for glob in CRLF_LINE_ENDING_FILES:
53        if fnmatch(file, glob):
54            return True
55    return False
56
57
58def check_line_endings(*files: str):
59    "Checks line endings. Windows only files are using clrf. All others just lf."
60    for line in cmd("git ls-files --eol", *files).lines():
61        parts = line.split()
62        file = parts[3]
63        index_endings = parts[0][2:]
64        wdir_endings = parts[1][2:]
65
66        def check_endings(endings: str):
67            if is_crlf_file(file):
68                if endings not in ("crlf", "mixed"):
69                    raise Exception(f"{file} Expected crlf file endings. Found {endings}")
70            else:
71                if endings in ("crlf", "mixed"):
72                    raise Exception(f"{file} Expected lf file endings. Found {endings}")
73
74        check_endings(index_endings)
75        check_endings(wdir_endings)
76
77
78def check_rust_lockfiles(*_files: str):
79    "Verifies that none of the Cargo.lock files require updates."
80    lockfiles = [Path("Cargo.lock"), *Path("common").glob("*/Cargo.lock")]
81    for path in lockfiles:
82        with cwd_context(path.parent):
83            if not cmd("cargo update --workspace --locked").success():
84                print(f"{path} is not up-to-date.")
85                print()
86                print("You may need to rebase your changes and run `cargo update --workspace`")
87                print("(or ./tools/run_tests) to ensure the Cargo.lock file is current.")
88                raise Exception("Cargo.lock out of date")
89
90
91# These crosvm features are currently not built upstream. Do not add to this list.
92KNOWN_DISABLED_FEATURES = [
93    "default-no-sandbox",
94    "direct",
95    "libvda",
96    "seccomp_trace",
97    "whpx",
98]
99
100
101def check_rust_features(*_files: str):
102    "Verifies that all cargo features are included in the list of features we compile upstream."
103    metadata = json.loads(cmd("cargo metadata --format-version=1").stdout())
104    crosvm_metadata = next(p for p in metadata["packages"] if p["name"] == "crosvm")
105    features = cast(Dict[str, List[str]], crosvm_metadata["features"])
106
107    def collect_features(feature_name: str) -> Generator[str, None, None]:
108        yield feature_name
109        for feature in features[feature_name]:
110            name = feature.split("/")[0]
111            if name in features:
112                yield from collect_features(name)
113
114    all_platform_features = set(
115        (
116            *collect_features("all-x86_64"),
117            *collect_features("all-aarch64"),
118            *collect_features("all-armhf"),
119            *collect_features("all-mingw64"),
120            *collect_features("all-msvc64"),
121        )
122    )
123    disabled_features = [
124        feature
125        for feature in features
126        if feature not in all_platform_features and feature not in KNOWN_DISABLED_FEATURES
127    ]
128    if disabled_features:
129        raise Exception(
130            f"The features {', '.join(disabled_features)} are not enabled in upstream crosvm builds."
131        )
132
133
134LICENSE_HEADER_RE = (
135    r".*Copyright (?P<year>20[0-9]{2})(?:-20[0-9]{2})? The ChromiumOS Authors\n"
136    r".*Use of this source code is governed by a BSD-style license that can be\n"
137    r".*found in the LICENSE file\.\n"
138    r"( *\*/\n)?"  # allow the end of a C-style comment before the blank line
139    r"\n"
140)
141
142NEW_LICENSE_HEADER = [
143    f"Copyright {datetime.now().year} The ChromiumOS Authors",
144    "Use of this source code is governed by a BSD-style license that can be",
145    "found in the LICENSE file.",
146]
147
148
149def new_licence_header(file_suffix: str):
150    if file_suffix in (".py", "", ".policy", ".sh"):
151        prefix = "#"
152    else:
153        prefix = "//"
154    return "\n".join(f"{prefix} {line}" for line in NEW_LICENSE_HEADER) + "\n\n"
155
156
157def check_copyright_header(*files: str, fix: bool = False):
158    "Checks copyright header. Can 'fix' them if needed by adding the header."
159    license_re = re.compile(LICENSE_HEADER_RE, re.MULTILINE)
160    for file_path in (Path(f) for f in files):
161        header = file_path.open("r").read(512)
162        license_match = license_re.search(header)
163        if license_match:
164            continue
165        # Generated files do not need a copyright header.
166        if "generated by" in header:
167            continue
168        if fix:
169            print(f"Adding copyright header: {file_path}")
170            contents = file_path.read_text()
171            file_path.write_text(new_licence_header(file_path.suffix) + contents)
172        else:
173            raise Exception(f"Bad copyright header: {file_path}")
174
175
176def check_file_ends_with_newline(*files: str, fix: bool = False):
177    "Checks if files end with a newline."
178    for file_path in (Path(f) for f in files):
179        with file_path.open("rb") as file:
180            # Skip empty files
181            file.seek(0, os.SEEK_END)
182            if file.tell() == 0:
183                continue
184            # Check last byte of the file
185            file.seek(-1, os.SEEK_END)
186            file_end = file.read(1)
187            if file_end.decode("utf-8") != "\n":
188                if fix:
189                    file_path.write_text(file_path.read_text() + "\n")
190                else:
191                    raise Exception(f"File does not end with a newline {file_path}")
192
193
194if __name__ == "__main__":
195    run_commands(
196        check_file_ends_with_newline,
197        check_copyright_header,
198        check_rust_features,
199        check_rust_lockfiles,
200        check_line_endings,
201        check_platform_independent,
202    )
203