123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- """
- 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
- from importlib.util import find_spec
- 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__(...)")
- if kwargs.get("with_pip", False):
- check_ensurepip()
- 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 check_ensurepip() -> None:
- """
- Check that we have ensurepip.
- Raise a fatal exception with a helpful hint if it isn't available.
- """
- if not find_spec("ensurepip"):
- msg = (
- "Python's ensurepip module is not found.\n"
- "It's normally part of the Python standard library, "
- "maybe your distribution packages it separately?\n"
- "Either install ensurepip, or alleviate the need for it in the "
- "first place by installing pip and setuptools for "
- f"'{sys.executable}'.\n"
- "(Hint: Debian puts ensurepip in its python3-venv package.)"
- )
- raise Ouch(msg)
- # ensurepip uses pyexpat, which can also go missing on us:
- if not find_spec("pyexpat"):
- msg = (
- "Python's pyexpat module is not found.\n"
- "It's normally part of the Python standard library, "
- "maybe your distribution packages it separately?\n"
- "Either install pyexpat, or alleviate the need for it in the "
- "first place by installing pip and setuptools for "
- f"'{sys.executable}'.\n\n"
- "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)"
- )
- raise Ouch(msg)
- 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())
|