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