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