1
0

presubmit 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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. import os
  6. import typing
  7. from typing import Generator, List, Literal, Optional, Tuple, Union
  8. from impl.common import (
  9. CROSVM_ROOT,
  10. TOOLS_ROOT,
  11. Triple,
  12. argh,
  13. chdir,
  14. cmd,
  15. run_main,
  16. )
  17. from impl.presubmit import Check, CheckContext, run_checks, Group
  18. python = cmd("python3")
  19. mypy = cmd("mypy").with_color_env("MYPY_FORCE_COLOR")
  20. black = cmd("black").with_color_arg(always="--color", never="--no-color")
  21. mdformat = cmd("mdformat")
  22. lucicfg = cmd("third_party/depot_tools/lucicfg")
  23. # All supported platforms as a type and a list.
  24. Platform = Literal["x86_64", "aarch64", "mingw64", "armhf"]
  25. PLATFORMS: Tuple[Platform, ...] = typing.get_args(Platform)
  26. ClippyOnlyPlatform = Literal["android"]
  27. CLIPPY_ONLY_PLATFORMS: Tuple[ClippyOnlyPlatform, ...] = typing.get_args(ClippyOnlyPlatform)
  28. def platform_is_supported(platform: Union[Platform, ClippyOnlyPlatform]):
  29. "Returns true if the platform is available as a target in rustup."
  30. triple = Triple.from_shorthand(platform)
  31. installed_toolchains = cmd("rustup target list --installed").lines()
  32. return str(triple) in installed_toolchains
  33. ####################################################################################################
  34. # Check methods
  35. #
  36. # Each check returns a Command (or list of Commands) to be run to execute the check. They are
  37. # registered and configured in the CHECKS list below.
  38. #
  39. # Some check functions are factory functions that return a check command for all supported
  40. # platforms.
  41. def check_python_tests(_: CheckContext):
  42. "Runs unit tests for python dev tooling."
  43. PYTHON_TESTS = [
  44. # Disabled due to b/309148074
  45. # "tests.cl_tests",
  46. "impl.common",
  47. ]
  48. return [python.with_cwd(TOOLS_ROOT).with_args("-m", file) for file in PYTHON_TESTS]
  49. def check_python_types(context: CheckContext):
  50. "Run mypy type checks on python dev tooling."
  51. return [mypy("--pretty", file) for file in context.all_files]
  52. def check_python_format(context: CheckContext):
  53. "Runs the black formatter on python dev tooling."
  54. return black.with_args(
  55. "--check" if not context.fix else None,
  56. *context.modified_files,
  57. )
  58. def check_markdown_format(context: CheckContext):
  59. "Runs mdformat on all markdown files."
  60. if "blaze" in mdformat("--version").stdout():
  61. raise Exception(
  62. "You are using google's mdformat. "
  63. + "Please update your PATH to ensure the pip installed mdformat is available."
  64. )
  65. return mdformat.with_args(
  66. "--wrap 100",
  67. "--check" if not context.fix else "",
  68. *context.modified_files,
  69. )
  70. def check_rust_format(context: CheckContext):
  71. "Runs rustfmt on all modified files."
  72. rustfmt = cmd(cmd("rustup +nightly which rustfmt").stdout())
  73. # Windows doesn't accept very long arguments: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa#:~:text=The%20maximum%20length%20of%20this%20string%20is%2032%2C767%20characters%2C%20including%20the%20Unicode%20terminating%20null%20character.%20If%20lpApplicationName%20is%20NULL%2C%20the%20module%20name%20portion%20of%20lpCommandLine%20is%20limited%20to%20MAX_PATH%20characters.
  74. return list(
  75. rustfmt.with_color_flag()
  76. .with_args("--check" if not context.fix else "")
  77. .foreach(context.modified_files, batch_size=10)
  78. )
  79. def check_cargo_doc(_: CheckContext):
  80. "Runs cargo-doc and verifies that no warnings are emitted."
  81. return cmd("./tools/cargo-doc").with_env("RUSTDOCFLAGS", "-D warnings").with_color_flag()
  82. def check_doc_tests(_: CheckContext):
  83. "Runs cargo doc tests. These cannot be run via nextest and run_tests."
  84. return cmd(
  85. "cargo test",
  86. "--doc",
  87. "--workspace",
  88. "--features=all-x86_64",
  89. ).with_color_flag()
  90. def check_mdbook(_: CheckContext):
  91. "Runs cargo-doc and verifies that no warnings are emitted."
  92. return cmd("mdbook build docs/book/")
  93. def check_crosvm_tests(platform: Platform):
  94. def check(_: CheckContext):
  95. if not platform_is_supported(platform):
  96. return None
  97. dut = None
  98. if os.access("/dev/kvm", os.W_OK):
  99. if platform == "x86_64":
  100. dut = "--dut=vm"
  101. elif platform == "aarch64":
  102. dut = "--dut=vm"
  103. return cmd("./tools/run_tests --verbose --platform", platform, dut).with_color_flag()
  104. check.__doc__ = f"Runs all crosvm tests for {platform}."
  105. return check
  106. def check_crosvm_unit_tests(platform: Platform):
  107. def check(_: CheckContext):
  108. if not platform_is_supported(platform):
  109. return None
  110. return cmd("./tools/run_tests --verbose --platform", platform).with_color_flag()
  111. check.__doc__ = f"Runs crosvm unit tests for {platform}."
  112. return check
  113. def check_crosvm_build(
  114. platform: Platform, features: Optional[str] = None, no_default_features: bool = False
  115. ):
  116. def check(_: CheckContext):
  117. return cmd(
  118. "./tools/run_tests --no-run --verbose --platform",
  119. platform,
  120. f"--features={features}" if features is not None else None,
  121. "--no-default-features" if no_default_features else None,
  122. ).with_color_flag()
  123. check.__doc__ = f"Builds crosvm for {platform} with features {features}."
  124. return check
  125. def check_clippy(platform: Union[Platform, ClippyOnlyPlatform]):
  126. def check(context: CheckContext):
  127. if not platform_is_supported(platform):
  128. return None
  129. return cmd(
  130. "./tools/clippy --platform",
  131. platform,
  132. "--fix" if context.fix else None,
  133. ).with_color_flag()
  134. check.__doc__ = f"Runs clippy for {platform}."
  135. return check
  136. def custom_check(name: str, can_fix: bool = False):
  137. "Custom checks are written in python in tools/custom_checks. This is a wrapper to call them."
  138. def check(context: CheckContext):
  139. return cmd(
  140. TOOLS_ROOT / "custom_checks",
  141. name,
  142. *context.modified_files,
  143. "--fix" if can_fix and context.fix else None,
  144. )
  145. check.__name__ = name.replace("-", "_")
  146. check.__doc__ = f"Runs tools/custom_check {name}"
  147. return check
  148. ####################################################################################################
  149. # Checks configuration
  150. #
  151. # Configures which checks are available and on which files they are run.
  152. # Check names default to the function name minus the check_ prefix
  153. CHECKS: List[Check] = [
  154. Check(
  155. check_rust_format,
  156. files=["**.rs"],
  157. exclude=["system_api/src/bindings/*"],
  158. can_fix=True,
  159. ),
  160. Check(
  161. check_mdbook,
  162. files=["docs/**/*"],
  163. ),
  164. Check(
  165. check_cargo_doc,
  166. files=["**.rs", "**Cargo.toml"],
  167. priority=True,
  168. ),
  169. Check(
  170. check_doc_tests,
  171. files=["**.rs", "**Cargo.toml"],
  172. priority=True,
  173. ),
  174. Check(
  175. check_python_tests,
  176. files=["tools/**.py"],
  177. python_tools=True,
  178. priority=True,
  179. ),
  180. Check(
  181. check_python_types,
  182. files=["tools/**.py"],
  183. exclude=[
  184. "tools/windows/*",
  185. "tools/contrib/memstats_chart/*",
  186. "tools/contrib/cros_tracing_analyser/*",
  187. ],
  188. python_tools=True,
  189. ),
  190. Check(
  191. check_python_format,
  192. files=["**.py"],
  193. python_tools=True,
  194. exclude=["infra/recipes.py"],
  195. can_fix=True,
  196. ),
  197. Check(
  198. check_markdown_format,
  199. files=["**.md"],
  200. exclude=[
  201. "infra/README.recipes.md",
  202. "docs/book/src/appendix/memory_layout.md",
  203. ],
  204. can_fix=True,
  205. ),
  206. *(
  207. Check(
  208. check_crosvm_build(platform, features="default"),
  209. custom_name=f"crosvm_build_default_{platform}",
  210. files=["**.rs"],
  211. priority=True,
  212. )
  213. for platform in PLATFORMS
  214. ),
  215. *(
  216. Check(
  217. check_crosvm_build(platform, features="", no_default_features=True),
  218. custom_name=f"crosvm_build_no_default_{platform}",
  219. files=["**.rs"],
  220. priority=True,
  221. )
  222. # TODO: b/260607247 crosvm does not compile with no-default-features on mingw64
  223. for platform in PLATFORMS
  224. if platform != "mingw64"
  225. ),
  226. *(
  227. Check(
  228. check_crosvm_tests(platform),
  229. custom_name=f"crosvm_tests_{platform}",
  230. files=["**.rs"],
  231. priority=True,
  232. )
  233. for platform in PLATFORMS
  234. ),
  235. *(
  236. Check(
  237. check_crosvm_unit_tests(platform),
  238. custom_name=f"crosvm_unit_tests_{platform}",
  239. files=["**.rs"],
  240. priority=True,
  241. )
  242. for platform in PLATFORMS
  243. ),
  244. *(
  245. Check(
  246. check_clippy(platform),
  247. custom_name=f"clippy_{platform}",
  248. files=["**.rs"],
  249. can_fix=True,
  250. priority=True,
  251. )
  252. for platform in (*PLATFORMS, *CLIPPY_ONLY_PLATFORMS)
  253. ),
  254. Check(
  255. custom_check("check-copyright-header"),
  256. files=["**.rs", "**.py", "**.c", "**.h", "**.policy", "**.sh"],
  257. exclude=[
  258. "infra/recipes.py",
  259. "hypervisor/src/whpx/whpx_sys/*.h",
  260. "third_party/vmm_vhost/*",
  261. "net_sys/src/lib.rs",
  262. "system_api/src/bindings/*",
  263. ],
  264. python_tools=True,
  265. can_fix=True,
  266. ),
  267. Check(
  268. custom_check("check-rust-features"),
  269. files=["**Cargo.toml"],
  270. ),
  271. Check(
  272. custom_check("check-rust-lockfiles"),
  273. files=["**Cargo.toml"],
  274. ),
  275. Check(
  276. custom_check("check-line-endings"),
  277. ),
  278. Check(
  279. custom_check("check-file-ends-with-newline"),
  280. exclude=[
  281. "**.h264",
  282. "**.vp8",
  283. "**.vp9",
  284. "**.ivf",
  285. "**.bin",
  286. "**.png",
  287. "**.min.js",
  288. "**.drawio",
  289. "**.json",
  290. "**.dtb",
  291. "**.dtbo",
  292. ],
  293. ),
  294. ]
  295. ####################################################################################################
  296. # Group configuration
  297. #
  298. # Configures pre-defined groups of checks. Some are configured for CI builders and others
  299. # are configured for convenience during local development.
  300. GROUPS: List[Group] = [
  301. # The default group is run if no check or group is explicitly set
  302. Group(
  303. name="default",
  304. doc="Checks run by default",
  305. checks=[
  306. "default_health_checks",
  307. # Run only one task per platform to prevent blocking on the build cache.
  308. "crosvm_tests_x86_64",
  309. "crosvm_unit_tests_aarch64",
  310. "crosvm_unit_tests_mingw64",
  311. "clippy_armhf",
  312. ],
  313. ),
  314. Group(
  315. name="quick",
  316. doc="Runs a quick subset of presubmit checks.",
  317. checks=[
  318. "default_health_checks",
  319. "crosvm_unit_tests_x86_64",
  320. "clippy_aarch64",
  321. ],
  322. ),
  323. Group(
  324. name="all",
  325. doc="Run checks of all builders.",
  326. checks=[
  327. "health_checks",
  328. *(f"linux_{platform}" for platform in PLATFORMS),
  329. *(f"clippy_{platform}" for platform in CLIPPY_ONLY_PLATFORMS),
  330. ],
  331. ),
  332. # Convenience groups for local usage:
  333. Group(
  334. name="clippy",
  335. doc="Runs clippy for all platforms",
  336. checks=[f"clippy_{platform}" for platform in PLATFORMS + CLIPPY_ONLY_PLATFORMS],
  337. ),
  338. Group(
  339. name="unit_tests",
  340. doc="Runs unit tests for all platforms",
  341. checks=[f"crosvm_unit_tests_{platform}" for platform in PLATFORMS],
  342. ),
  343. Group(
  344. name="format",
  345. doc="Runs all formatting checks (or fixes)",
  346. checks=[
  347. "rust_format",
  348. "markdown_format",
  349. "python_format",
  350. ],
  351. ),
  352. Group(
  353. name="default_health_checks",
  354. doc="Health checks to run by default",
  355. checks=[
  356. # Check if lockfiles need updating first. Otherwise another step may do the update.
  357. "rust_lockfiles",
  358. "copyright_header",
  359. "file_ends_with_newline",
  360. "line_endings",
  361. "markdown_format",
  362. "mdbook",
  363. "cargo_doc",
  364. "python_format",
  365. "python_types",
  366. "rust_features",
  367. "rust_format",
  368. ],
  369. ),
  370. # The groups below are used by builders in CI:
  371. Group(
  372. name="health_checks",
  373. doc="Checks run on the health_check builder",
  374. checks=[
  375. "default_health_checks",
  376. "doc_tests",
  377. "python_tests",
  378. ],
  379. ),
  380. Group(
  381. name="android-aarch64",
  382. doc="Checks run on the android-aarch64 builder",
  383. checks=[
  384. "clippy_android",
  385. ],
  386. ),
  387. *(
  388. Group(
  389. name=f"linux_{platform}",
  390. doc=f"Checks run on the linux-{platform} builder",
  391. checks=[
  392. f"crosvm_tests_{platform}",
  393. f"clippy_{platform}",
  394. f"crosvm_build_default_{platform}",
  395. ]
  396. # TODO: b/260607247 crosvm does not compile with no-default-features on mingw64
  397. + ([f"crosvm_build_no_default_{platform}"] if platform != "mingw64" else []),
  398. )
  399. for platform in PLATFORMS
  400. ),
  401. ]
  402. # Turn both lists into dicts for convenience
  403. CHECKS_DICT = dict((c.name, c) for c in CHECKS)
  404. GROUPS_DICT = dict((c.name, c) for c in GROUPS)
  405. def validate_config():
  406. "Validates the CHECKS and GROUPS configuration."
  407. for group in GROUPS:
  408. for check in group.checks:
  409. if check not in CHECKS_DICT and check not in GROUPS_DICT:
  410. raise Exception(f"Group {group.name} includes non-existing item {check}.")
  411. def find_in_group(check: Check):
  412. for group in GROUPS:
  413. if check.name in group.checks:
  414. return True
  415. return False
  416. for check in CHECKS:
  417. if not find_in_group(check):
  418. raise Exception(f"Check {check.name} is not included in any group.")
  419. all_names = [c.name for c in CHECKS] + [g.name for g in GROUPS]
  420. for name in all_names:
  421. if all_names.count(name) > 1:
  422. raise Exception(f"Check or group {name} is defined multiple times.")
  423. def get_check_names_in_group(group: Group) -> Generator[str, None, None]:
  424. for name in group.checks:
  425. if name in GROUPS_DICT:
  426. yield from get_check_names_in_group(GROUPS_DICT[name])
  427. else:
  428. yield name
  429. @argh.arg("--list-checks", default=False, help="List names of available checks and exit.")
  430. @argh.arg("--fix", default=False, help="Asks checks to fix problems where possible.")
  431. @argh.arg("--no-delta", default=False, help="Run on all files instead of just modified files.")
  432. @argh.arg("--no-parallel", default=False, help="Do not run checks in parallel.")
  433. @argh.arg(
  434. "checks_or_groups",
  435. help="List of checks or groups to run. Defaults to run the `default` group.",
  436. )
  437. def main(
  438. list_checks: bool = False,
  439. fix: bool = False,
  440. no_delta: bool = False,
  441. no_parallel: bool = False,
  442. *checks_or_groups: str,
  443. ):
  444. chdir(CROSVM_ROOT)
  445. validate_config()
  446. if not checks_or_groups:
  447. checks_or_groups = ("default",)
  448. # Resolve and validate the groups and checks provided
  449. check_names: List[str] = []
  450. for check_or_group in checks_or_groups:
  451. if check_or_group in CHECKS_DICT:
  452. check_names.append(check_or_group)
  453. elif check_or_group in GROUPS_DICT:
  454. check_names += list(get_check_names_in_group(GROUPS_DICT[check_or_group]))
  455. else:
  456. raise Exception(f"No such check or group: {check_or_group}")
  457. # Remove duplicates while preserving order
  458. check_names = list(dict.fromkeys(check_names))
  459. if list_checks:
  460. for check in check_names:
  461. print(check)
  462. return
  463. check_list = [CHECKS_DICT[name] for name in check_names]
  464. run_checks(
  465. check_list,
  466. fix=fix,
  467. run_on_all_files=no_delta,
  468. parallel=not no_parallel,
  469. )
  470. def usage():
  471. groups = "\n".join(f" {group.name}: {group.doc}" for group in GROUPS)
  472. checks = "\n".join(f" {check.name}: {check.doc}" for check in CHECKS)
  473. return f"""\
  474. Runs checks on the crosvm codebase.
  475. Basic usage, to run a default selection of checks:
  476. ./tools/presubmit
  477. Some checkers can fix issues they find (e.g. formatters, clippy, etc):
  478. ./tools/presubmit --fix
  479. Various groups of presubmit checks can be run via:
  480. ./tools/presubmit group_name
  481. Available groups are:
  482. {groups}
  483. You can also provide the names of specific checks to run:
  484. ./tools/presubmit check1 check2
  485. Available checks are:
  486. {checks}
  487. """
  488. if __name__ == "__main__":
  489. run_main(main, usage=usage())