| """ |
| mkvenv - QEMU pyvenv bootstrapping utility |
| |
| usage: mkvenv [-h] command ... |
| |
| QEMU pyvenv bootstrapping utility |
| |
| options: |
| -h, --help show this help message and exit |
| |
| Commands: |
| command Description |
| create create a venv |
| |
| -------------------------------------------------- |
| |
| usage: mkvenv create [-h] target |
| |
| positional arguments: |
| target Target directory to install virtual environment into. |
| |
| options: |
| -h, --help show this help message and exit |
| |
| """ |
| |
| # Copyright (C) 2022-2023 Red Hat, Inc. |
| # |
| # Authors: |
| # John Snow <jsnow@redhat.com> |
| # Paolo Bonzini <pbonzini@redhat.com> |
| # |
| # This work is licensed under the terms of the GNU GPL, version 2 or |
| # later. See the COPYING file in the top-level directory. |
| |
| import argparse |
| import logging |
| import os |
| from pathlib import Path |
| import subprocess |
| import sys |
| from types import SimpleNamespace |
| from typing import Any, Optional, Union |
| import venv |
| |
| |
| # Do not add any mandatory dependencies from outside the stdlib: |
| # This script *must* be usable standalone! |
| |
| DirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] |
| logger = logging.getLogger("mkvenv") |
| |
| |
| class Ouch(RuntimeError): |
| """An Exception class we can't confuse with a builtin.""" |
| |
| |
| class QemuEnvBuilder(venv.EnvBuilder): |
| """ |
| An extension of venv.EnvBuilder for building QEMU's configure-time venv. |
| |
| As of this commit, it does not yet do anything particularly |
| different than the standard venv-creation utility. The next several |
| commits will gradually change that in small commits that highlight |
| each feature individually. |
| |
| Parameters for base class init: |
| - system_site_packages: bool = False |
| - clear: bool = False |
| - symlinks: bool = False |
| - upgrade: bool = False |
| - with_pip: bool = False |
| - prompt: Optional[str] = None |
| - upgrade_deps: bool = False (Since 3.9) |
| """ |
| |
| def __init__(self, *args: Any, **kwargs: Any) -> None: |
| logger.debug("QemuEnvBuilder.__init__(...)") |
| super().__init__(*args, **kwargs) |
| |
| # Make the context available post-creation: |
| self._context: Optional[SimpleNamespace] = None |
| |
| def ensure_directories(self, env_dir: DirType) -> SimpleNamespace: |
| logger.debug("ensure_directories(env_dir=%s)", env_dir) |
| self._context = super().ensure_directories(env_dir) |
| return self._context |
| |
| def get_value(self, field: str) -> str: |
| """ |
| Get a string value from the context namespace after a call to build. |
| |
| For valid field names, see: |
| https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories |
| """ |
| ret = getattr(self._context, field) |
| assert isinstance(ret, str) |
| return ret |
| |
| |
| def make_venv( # pylint: disable=too-many-arguments |
| env_dir: Union[str, Path], |
| system_site_packages: bool = False, |
| clear: bool = True, |
| symlinks: Optional[bool] = None, |
| with_pip: bool = True, |
| ) -> None: |
| """ |
| Create a venv using `QemuEnvBuilder`. |
| |
| This is analogous to the `venv.create` module-level convenience |
| function that is part of the Python stdblib, except it uses |
| `QemuEnvBuilder` instead. |
| |
| :param env_dir: The directory to create/install to. |
| :param system_site_packages: |
| Allow inheriting packages from the system installation. |
| :param clear: When True, fully remove any prior venv and files. |
| :param symlinks: |
| Whether to use symlinks to the target interpreter or not. If |
| left unspecified, it will use symlinks except on Windows to |
| match behavior with the "venv" CLI tool. |
| :param with_pip: |
| Whether to install "pip" binaries or not. |
| """ |
| logger.debug( |
| "%s: make_venv(env_dir=%s, system_site_packages=%s, " |
| "clear=%s, symlinks=%s, with_pip=%s)", |
| __file__, |
| str(env_dir), |
| system_site_packages, |
| clear, |
| symlinks, |
| with_pip, |
| ) |
| |
| if symlinks is None: |
| # Default behavior of standard venv CLI |
| symlinks = os.name != "nt" |
| |
| builder = QemuEnvBuilder( |
| system_site_packages=system_site_packages, |
| clear=clear, |
| symlinks=symlinks, |
| with_pip=with_pip, |
| ) |
| |
| style = "non-isolated" if builder.system_site_packages else "isolated" |
| print( |
| f"mkvenv: Creating {style} virtual environment" |
| f" at '{str(env_dir)}'", |
| file=sys.stderr, |
| ) |
| |
| try: |
| logger.debug("Invoking builder.create()") |
| try: |
| builder.create(str(env_dir)) |
| except SystemExit as exc: |
| # Some versions of the venv module raise SystemExit; *nasty*! |
| # We want the exception that prompted it. It might be a subprocess |
| # error that has output we *really* want to see. |
| logger.debug("Intercepted SystemExit from EnvBuilder.create()") |
| raise exc.__cause__ or exc.__context__ or exc |
| logger.debug("builder.create() finished") |
| except subprocess.CalledProcessError as exc: |
| logger.error("mkvenv subprocess failed:") |
| logger.error("cmd: %s", exc.cmd) |
| logger.error("returncode: %d", exc.returncode) |
| |
| def _stringify(data: Union[str, bytes]) -> str: |
| if isinstance(data, bytes): |
| return data.decode() |
| return data |
| |
| lines = [] |
| if exc.stdout: |
| lines.append("========== stdout ==========") |
| lines.append(_stringify(exc.stdout)) |
| lines.append("============================") |
| if exc.stderr: |
| lines.append("========== stderr ==========") |
| lines.append(_stringify(exc.stderr)) |
| lines.append("============================") |
| if lines: |
| logger.error(os.linesep.join(lines)) |
| |
| raise Ouch("VENV creation subprocess failed.") from exc |
| |
| # print the python executable to stdout for configure. |
| print(builder.get_value("env_exe")) |
| |
| |
| def _add_create_subcommand(subparsers: Any) -> None: |
| subparser = subparsers.add_parser("create", help="create a venv") |
| subparser.add_argument( |
| "target", |
| type=str, |
| action="store", |
| help="Target directory to install virtual environment into.", |
| ) |
| |
| |
| def main() -> int: |
| """CLI interface to make_qemu_venv. See module docstring.""" |
| if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"): |
| # You're welcome. |
| logging.basicConfig(level=logging.DEBUG) |
| elif os.environ.get("V"): |
| logging.basicConfig(level=logging.INFO) |
| |
| parser = argparse.ArgumentParser( |
| prog="mkvenv", |
| description="QEMU pyvenv bootstrapping utility", |
| ) |
| subparsers = parser.add_subparsers( |
| title="Commands", |
| dest="command", |
| metavar="command", |
| help="Description", |
| ) |
| |
| _add_create_subcommand(subparsers) |
| |
| args = parser.parse_args() |
| try: |
| if args.command == "create": |
| make_venv( |
| args.target, |
| system_site_packages=True, |
| clear=True, |
| ) |
| logger.debug("mkvenv.py %s: exiting", args.command) |
| except Ouch as exc: |
| print("\n*** Ouch! ***\n", file=sys.stderr) |
| print(str(exc), "\n\n", file=sys.stderr) |
| return 1 |
| except SystemExit: |
| raise |
| except: # pylint: disable=bare-except |
| logger.exception("mkvenv did not complete successfully:") |
| return 2 |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |