custom_checks 7.4 KB

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