util.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. #!/usr/bin/env python3
  2. # Copyright 2023 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. Provides general utility functions.
  7. """
  8. import argparse
  9. import contextlib
  10. import datetime
  11. import functools
  12. import os
  13. import re
  14. import subprocess
  15. import sys
  16. import urllib
  17. import urllib.request
  18. import urllib.error
  19. from pathlib import Path
  20. from subprocess import DEVNULL, PIPE, STDOUT # type: ignore
  21. from typing import (
  22. Dict,
  23. List,
  24. NamedTuple,
  25. Optional,
  26. Tuple,
  27. Union,
  28. )
  29. PathLike = Union[Path, str]
  30. # Regex that matches ANSI escape sequences
  31. ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
  32. def find_crosvm_root():
  33. "Walk up from CWD until we find the crosvm root dir."
  34. path = Path("").resolve()
  35. while True:
  36. if (path / "tools/impl/common.py").is_file():
  37. return path
  38. if path.parent:
  39. path = path.parent
  40. else:
  41. raise Exception("Cannot find crosvm root dir.")
  42. "Root directory of crosvm derived from CWD."
  43. CROSVM_ROOT = find_crosvm_root()
  44. "Cargo.toml file of crosvm"
  45. CROSVM_TOML = CROSVM_ROOT / "Cargo.toml"
  46. """
  47. Root directory of crosvm devtools.
  48. May be different from `CROSVM_ROOT/tools`, which is allows you to run the crosvm dev
  49. tools from this directory on another crosvm repo.
  50. Use this if you want to call crosvm dev tools, which will use the scripts relative
  51. to this file.
  52. """
  53. TOOLS_ROOT = Path(__file__).parent.parent.resolve()
  54. "Cache directory that is preserved between builds in CI."
  55. CACHE_DIR = Path(os.environ.get("CROSVM_CACHE_DIR", os.environ.get("TMPDIR", "/tmp")))
  56. # Ensure that we really found the crosvm root directory
  57. assert 'name = "crosvm"' in CROSVM_TOML.read_text()
  58. # List of times recorded by `record_time` which will be printed if --timing-info is provided.
  59. global_time_records: List[Tuple[str, datetime.timedelta]] = []
  60. def crosvm_target_dir():
  61. crosvm_target = os.environ.get("CROSVM_TARGET_DIR")
  62. cargo_target = os.environ.get("CARGO_TARGET_DIR")
  63. if crosvm_target:
  64. return Path(crosvm_target)
  65. elif cargo_target:
  66. return Path(cargo_target) / "crosvm"
  67. else:
  68. return CROSVM_ROOT / "target/crosvm"
  69. @functools.lru_cache(None)
  70. def parse_common_args():
  71. """
  72. Parse args common to all scripts
  73. These args are parsed separately of the run_main/run_commands method so we can access
  74. verbose/etc before the commands arguments are parsed.
  75. """
  76. parser = argparse.ArgumentParser(add_help=False)
  77. add_common_args(parser)
  78. return parser.parse_known_args()[0]
  79. def add_common_args(parser: argparse.ArgumentParser):
  80. "These args are added to all commands."
  81. parser.add_argument(
  82. "--color",
  83. default="auto",
  84. choices=("always", "never", "auto"),
  85. help="Force enable or disable colors. Defaults to automatic detection.",
  86. )
  87. parser.add_argument(
  88. "--verbose",
  89. "-v",
  90. action="store_true",
  91. default=False,
  92. help="Print more details about the commands this script is running.",
  93. )
  94. parser.add_argument(
  95. "--very-verbose",
  96. "-vv",
  97. action="store_true",
  98. default=False,
  99. help="Print more debug output",
  100. )
  101. parser.add_argument(
  102. "--timing-info",
  103. action="store_true",
  104. default=False,
  105. help="Print info on how long which parts of the command take",
  106. )
  107. def verbose():
  108. return very_verbose() or parse_common_args().verbose
  109. def very_verbose():
  110. return parse_common_args().very_verbose
  111. def color_enabled():
  112. color_arg = parse_common_args().color
  113. if color_arg == "never":
  114. return False
  115. if color_arg == "always":
  116. return True
  117. return sys.stdout.isatty()
  118. def find_scripts(path: Path, shebang: str):
  119. for file in path.glob("*"):
  120. if file.is_file() and file.open(errors="ignore").read(512).startswith(f"#!{shebang}"):
  121. yield file
  122. def confirm(message: str, default: bool = False):
  123. print(message, "[y/N]" if default == False else "[Y/n]", end=" ", flush=True)
  124. response = sys.stdin.readline().strip()
  125. if response in ("y", "Y"):
  126. return True
  127. if response in ("n", "N"):
  128. return False
  129. return default
  130. def is_cros_repo():
  131. "Returns true if the crosvm repo is a symlink or worktree to a CrOS repo checkout."
  132. dot_git = CROSVM_ROOT / ".git"
  133. if not dot_git.is_symlink() and dot_git.is_dir():
  134. return False
  135. return (cros_repo_root() / ".repo").exists()
  136. def cros_repo_root():
  137. "Root directory of the CrOS repo checkout."
  138. return (CROSVM_ROOT / "../../..").resolve()
  139. def is_kiwi_repo():
  140. "Returns true if the crosvm repo contains .kiwi_repo file."
  141. dot_kiwi_repo = CROSVM_ROOT / ".kiwi_repo"
  142. return dot_kiwi_repo.exists()
  143. def kiwi_repo_root():
  144. "Root directory of the kiwi repo checkout."
  145. return (CROSVM_ROOT / "../..").resolve()
  146. def is_aosp_repo():
  147. "Returns true if the crosvm repo is an AOSP repo checkout."
  148. android_bp = CROSVM_ROOT / "Android.bp"
  149. return android_bp.exists()
  150. def aosp_repo_root():
  151. "Root directory of AOSP repo checkout."
  152. return (CROSVM_ROOT / "../..").resolve()
  153. def sudo_is_passwordless():
  154. # Run with --askpass but no askpass set, succeeds only if passwordless sudo
  155. # is available.
  156. (ret, _) = subprocess.getstatusoutput("SUDO_ASKPASS=false sudo --askpass true")
  157. return ret == 0
  158. SHORTHANDS = {
  159. "mingw64": "x86_64-pc-windows-gnu",
  160. "msvc64": "x86_64-pc-windows-msvc",
  161. "armhf": "armv7-unknown-linux-gnueabihf",
  162. "aarch64": "aarch64-unknown-linux-gnu",
  163. "riscv64": "riscv64gc-unknown-linux-gnu",
  164. "x86_64": "x86_64-unknown-linux-gnu",
  165. "android": "aarch64-linux-android",
  166. }
  167. class Triple(NamedTuple):
  168. """
  169. Build triple in cargo format.
  170. The format is: <arch><sub>-<vendor>-<sys>-<abi>, However, we will treat <arch><sub> as a single
  171. arch to simplify things.
  172. """
  173. arch: str
  174. vendor: str
  175. sys: Optional[str]
  176. abi: Optional[str]
  177. @classmethod
  178. def from_shorthand(cls, shorthand: str):
  179. "These shorthands make it easier to specify triples on the command line."
  180. if "-" in shorthand:
  181. triple = shorthand
  182. elif shorthand in SHORTHANDS:
  183. triple = SHORTHANDS[shorthand]
  184. else:
  185. raise Exception(f"Not a valid build triple shorthand: {shorthand}")
  186. return cls.from_str(triple)
  187. @classmethod
  188. def from_str(cls, triple: str):
  189. parts = triple.split("-")
  190. if len(parts) < 2:
  191. raise Exception(f"Unsupported triple {triple}")
  192. return cls(
  193. parts[0],
  194. parts[1],
  195. parts[2] if len(parts) > 2 else None,
  196. parts[3] if len(parts) > 3 else None,
  197. )
  198. @classmethod
  199. def from_linux_arch(cls, arch: str):
  200. "Rough logic to convert the output of `arch` into a corresponding linux build triple."
  201. if arch == "armhf":
  202. return cls.from_str("armv7-unknown-linux-gnueabihf")
  203. else:
  204. return cls.from_str(f"{arch}-unknown-linux-gnu")
  205. @classmethod
  206. def host_default(cls):
  207. "Returns the default build triple of the host."
  208. rustc_info = subprocess.check_output(["rustc", "-vV"], text=True)
  209. match = re.search(r"host: (\S+)", rustc_info)
  210. if not match:
  211. raise Exception(f"Cannot parse rustc info: {rustc_info}")
  212. return cls.from_str(match.group(1))
  213. @property
  214. def feature_flag(self):
  215. triple_to_shorthand = {v: k for k, v in SHORTHANDS.items()}
  216. shorthand = triple_to_shorthand.get(str(self))
  217. if not shorthand:
  218. raise Exception(f"No feature set for triple {self}")
  219. return f"all-{shorthand}"
  220. @property
  221. def target_dir(self):
  222. return crosvm_target_dir() / str(self)
  223. def get_cargo_env(self):
  224. """Environment variables to make cargo use the test target."""
  225. env: Dict[str, str] = {}
  226. cargo_target = str(self)
  227. env["CARGO_BUILD_TARGET"] = cargo_target
  228. env["CARGO_TARGET_DIR"] = str(self.target_dir)
  229. env["CROSVM_TARGET_DIR"] = str(crosvm_target_dir())
  230. # Android builds are not fully supported and can only be used to run clippy.
  231. # Underlying libraries (e.g. minijail) will be built for linux instead
  232. # TODO(denniskempin): This could be better done with [env] in Cargo.toml if it supported
  233. # per-target configuration. See https://github.com/rust-lang/cargo/issues/10273
  234. if str(self).endswith("-linux-android"):
  235. env["MINIJAIL_DO_NOT_BUILD"] = "true"
  236. env["MINIJAIL_BINDGEN_TARGET"] = f"{self.arch}-unknown-linux-gnu"
  237. return env
  238. def __str__(self):
  239. parts = [self.arch, self.vendor]
  240. if self.sys:
  241. parts = [*parts, self.sys]
  242. if self.abi:
  243. parts = [*parts, self.abi]
  244. return "-".join(parts)
  245. def download_file(url: str, filename: Path, attempts: int = 3):
  246. assert attempts > 0
  247. while True:
  248. attempts -= 1
  249. try:
  250. urllib.request.urlretrieve(url, filename)
  251. return
  252. except Exception as e:
  253. if attempts == 0:
  254. raise e
  255. else:
  256. print("Download failed:", e)
  257. def strip_ansi_escape_sequences(line: str) -> str:
  258. return ANSI_ESCAPE.sub("", line)
  259. def ensure_packages_exist(*packages: str):
  260. """
  261. Exits if one of the listed packages does not exist.
  262. """
  263. missing_packages: List[str] = []
  264. for package in packages:
  265. try:
  266. __import__(package)
  267. except ImportError:
  268. missing_packages.append(package)
  269. if missing_packages:
  270. debian_packages = [f"python3-{p}" for p in missing_packages]
  271. package_list = " ".join(debian_packages)
  272. print("Missing python dependencies. Please re-run ./tools/install-deps")
  273. print(f"Or `sudo apt install {package_list}`")
  274. sys.exit(1)
  275. @contextlib.contextmanager
  276. def record_time(title: str):
  277. """
  278. Records wall-time of how long this context lasts.
  279. The results will be printed at the end of script executation if --timing-info is specified.
  280. """
  281. start_time = datetime.datetime.now()
  282. try:
  283. yield
  284. finally:
  285. global_time_records.append((title, datetime.datetime.now() - start_time))
  286. def print_timing_info():
  287. print()
  288. print("Timing info:")
  289. print()
  290. for title, delta in global_time_records:
  291. print(f" {title:20} {delta.total_seconds():.2f}s")