123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- #!/usr/bin/env python3
- # Copyright 2023 The ChromiumOS Authors
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """
- Provides general utility functions.
- """
- import argparse
- import contextlib
- import datetime
- import functools
- import os
- import re
- import subprocess
- import sys
- import urllib
- import urllib.request
- import urllib.error
- from pathlib import Path
- from subprocess import DEVNULL, PIPE, STDOUT # type: ignore
- from typing import (
- Dict,
- List,
- NamedTuple,
- Optional,
- Tuple,
- Union,
- )
- PathLike = Union[Path, str]
- # Regex that matches ANSI escape sequences
- ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
- def find_crosvm_root():
- "Walk up from CWD until we find the crosvm root dir."
- path = Path("").resolve()
- while True:
- if (path / "tools/impl/common.py").is_file():
- return path
- if path.parent:
- path = path.parent
- else:
- raise Exception("Cannot find crosvm root dir.")
- "Root directory of crosvm derived from CWD."
- CROSVM_ROOT = find_crosvm_root()
- "Cargo.toml file of crosvm"
- CROSVM_TOML = CROSVM_ROOT / "Cargo.toml"
- """
- Root directory of crosvm devtools.
- May be different from `CROSVM_ROOT/tools`, which is allows you to run the crosvm dev
- tools from this directory on another crosvm repo.
- Use this if you want to call crosvm dev tools, which will use the scripts relative
- to this file.
- """
- TOOLS_ROOT = Path(__file__).parent.parent.resolve()
- "Cache directory that is preserved between builds in CI."
- CACHE_DIR = Path(os.environ.get("CROSVM_CACHE_DIR", os.environ.get("TMPDIR", "/tmp")))
- # Ensure that we really found the crosvm root directory
- assert 'name = "crosvm"' in CROSVM_TOML.read_text()
- # List of times recorded by `record_time` which will be printed if --timing-info is provided.
- global_time_records: List[Tuple[str, datetime.timedelta]] = []
- def crosvm_target_dir():
- crosvm_target = os.environ.get("CROSVM_TARGET_DIR")
- cargo_target = os.environ.get("CARGO_TARGET_DIR")
- if crosvm_target:
- return Path(crosvm_target)
- elif cargo_target:
- return Path(cargo_target) / "crosvm"
- else:
- return CROSVM_ROOT / "target/crosvm"
- @functools.lru_cache(None)
- def parse_common_args():
- """
- Parse args common to all scripts
- These args are parsed separately of the run_main/run_commands method so we can access
- verbose/etc before the commands arguments are parsed.
- """
- parser = argparse.ArgumentParser(add_help=False)
- add_common_args(parser)
- return parser.parse_known_args()[0]
- def add_common_args(parser: argparse.ArgumentParser):
- "These args are added to all commands."
- parser.add_argument(
- "--color",
- default="auto",
- choices=("always", "never", "auto"),
- help="Force enable or disable colors. Defaults to automatic detection.",
- )
- parser.add_argument(
- "--verbose",
- "-v",
- action="store_true",
- default=False,
- help="Print more details about the commands this script is running.",
- )
- parser.add_argument(
- "--very-verbose",
- "-vv",
- action="store_true",
- default=False,
- help="Print more debug output",
- )
- parser.add_argument(
- "--timing-info",
- action="store_true",
- default=False,
- help="Print info on how long which parts of the command take",
- )
- def verbose():
- return very_verbose() or parse_common_args().verbose
- def very_verbose():
- return parse_common_args().very_verbose
- def color_enabled():
- color_arg = parse_common_args().color
- if color_arg == "never":
- return False
- if color_arg == "always":
- return True
- return sys.stdout.isatty()
- def find_scripts(path: Path, shebang: str):
- for file in path.glob("*"):
- if file.is_file() and file.open(errors="ignore").read(512).startswith(f"#!{shebang}"):
- yield file
- def confirm(message: str, default: bool = False):
- print(message, "[y/N]" if default == False else "[Y/n]", end=" ", flush=True)
- response = sys.stdin.readline().strip()
- if response in ("y", "Y"):
- return True
- if response in ("n", "N"):
- return False
- return default
- def is_cros_repo():
- "Returns true if the crosvm repo is a symlink or worktree to a CrOS repo checkout."
- dot_git = CROSVM_ROOT / ".git"
- if not dot_git.is_symlink() and dot_git.is_dir():
- return False
- return (cros_repo_root() / ".repo").exists()
- def cros_repo_root():
- "Root directory of the CrOS repo checkout."
- return (CROSVM_ROOT / "../../..").resolve()
- def is_kiwi_repo():
- "Returns true if the crosvm repo contains .kiwi_repo file."
- dot_kiwi_repo = CROSVM_ROOT / ".kiwi_repo"
- return dot_kiwi_repo.exists()
- def kiwi_repo_root():
- "Root directory of the kiwi repo checkout."
- return (CROSVM_ROOT / "../..").resolve()
- def is_aosp_repo():
- "Returns true if the crosvm repo is an AOSP repo checkout."
- android_bp = CROSVM_ROOT / "Android.bp"
- return android_bp.exists()
- def aosp_repo_root():
- "Root directory of AOSP repo checkout."
- return (CROSVM_ROOT / "../..").resolve()
- def sudo_is_passwordless():
- # Run with --askpass but no askpass set, succeeds only if passwordless sudo
- # is available.
- (ret, _) = subprocess.getstatusoutput("SUDO_ASKPASS=false sudo --askpass true")
- return ret == 0
- SHORTHANDS = {
- "mingw64": "x86_64-pc-windows-gnu",
- "msvc64": "x86_64-pc-windows-msvc",
- "armhf": "armv7-unknown-linux-gnueabihf",
- "aarch64": "aarch64-unknown-linux-gnu",
- "riscv64": "riscv64gc-unknown-linux-gnu",
- "x86_64": "x86_64-unknown-linux-gnu",
- "android": "aarch64-linux-android",
- }
- class Triple(NamedTuple):
- """
- Build triple in cargo format.
- The format is: <arch><sub>-<vendor>-<sys>-<abi>, However, we will treat <arch><sub> as a single
- arch to simplify things.
- """
- arch: str
- vendor: str
- sys: Optional[str]
- abi: Optional[str]
- @classmethod
- def from_shorthand(cls, shorthand: str):
- "These shorthands make it easier to specify triples on the command line."
- if "-" in shorthand:
- triple = shorthand
- elif shorthand in SHORTHANDS:
- triple = SHORTHANDS[shorthand]
- else:
- raise Exception(f"Not a valid build triple shorthand: {shorthand}")
- return cls.from_str(triple)
- @classmethod
- def from_str(cls, triple: str):
- parts = triple.split("-")
- if len(parts) < 2:
- raise Exception(f"Unsupported triple {triple}")
- return cls(
- parts[0],
- parts[1],
- parts[2] if len(parts) > 2 else None,
- parts[3] if len(parts) > 3 else None,
- )
- @classmethod
- def from_linux_arch(cls, arch: str):
- "Rough logic to convert the output of `arch` into a corresponding linux build triple."
- if arch == "armhf":
- return cls.from_str("armv7-unknown-linux-gnueabihf")
- else:
- return cls.from_str(f"{arch}-unknown-linux-gnu")
- @classmethod
- def host_default(cls):
- "Returns the default build triple of the host."
- rustc_info = subprocess.check_output(["rustc", "-vV"], text=True)
- match = re.search(r"host: (\S+)", rustc_info)
- if not match:
- raise Exception(f"Cannot parse rustc info: {rustc_info}")
- return cls.from_str(match.group(1))
- @property
- def feature_flag(self):
- triple_to_shorthand = {v: k for k, v in SHORTHANDS.items()}
- shorthand = triple_to_shorthand.get(str(self))
- if not shorthand:
- raise Exception(f"No feature set for triple {self}")
- return f"all-{shorthand}"
- @property
- def target_dir(self):
- return crosvm_target_dir() / str(self)
- def get_cargo_env(self):
- """Environment variables to make cargo use the test target."""
- env: Dict[str, str] = {}
- cargo_target = str(self)
- env["CARGO_BUILD_TARGET"] = cargo_target
- env["CARGO_TARGET_DIR"] = str(self.target_dir)
- env["CROSVM_TARGET_DIR"] = str(crosvm_target_dir())
- # Android builds are not fully supported and can only be used to run clippy.
- # Underlying libraries (e.g. minijail) will be built for linux instead
- # TODO(denniskempin): This could be better done with [env] in Cargo.toml if it supported
- # per-target configuration. See https://github.com/rust-lang/cargo/issues/10273
- if str(self).endswith("-linux-android"):
- env["MINIJAIL_DO_NOT_BUILD"] = "true"
- env["MINIJAIL_BINDGEN_TARGET"] = f"{self.arch}-unknown-linux-gnu"
- return env
- def __str__(self):
- parts = [self.arch, self.vendor]
- if self.sys:
- parts = [*parts, self.sys]
- if self.abi:
- parts = [*parts, self.abi]
- return "-".join(parts)
- def download_file(url: str, filename: Path, attempts: int = 3):
- assert attempts > 0
- while True:
- attempts -= 1
- try:
- urllib.request.urlretrieve(url, filename)
- return
- except Exception as e:
- if attempts == 0:
- raise e
- else:
- print("Download failed:", e)
- def strip_ansi_escape_sequences(line: str) -> str:
- return ANSI_ESCAPE.sub("", line)
- def ensure_packages_exist(*packages: str):
- """
- Exits if one of the listed packages does not exist.
- """
- missing_packages: List[str] = []
- for package in packages:
- try:
- __import__(package)
- except ImportError:
- missing_packages.append(package)
- if missing_packages:
- debian_packages = [f"python3-{p}" for p in missing_packages]
- package_list = " ".join(debian_packages)
- print("Missing python dependencies. Please re-run ./tools/install-deps")
- print(f"Or `sudo apt install {package_list}`")
- sys.exit(1)
- @contextlib.contextmanager
- def record_time(title: str):
- """
- Records wall-time of how long this context lasts.
- The results will be printed at the end of script executation if --timing-info is specified.
- """
- start_time = datetime.datetime.now()
- try:
- yield
- finally:
- global_time_records.append((title, datetime.datetime.now() - start_time))
- def print_timing_info():
- print()
- print("Timing info:")
- print()
- for title, delta in global_time_records:
- print(f" {title:20} {delta.total_seconds():.2f}s")
|