This reverts commit e8e4298feadae7924cf7600bb3bcc5b0a8d7cbe9. ensuregroup allows to specify both the acceptable versions of avocado, and a locked version to be used when avocado is not installed as a system pacakge. This lets us install avocado in pyvenv/ using "mkvenv.py" and reuse the distro package on Fedora and CentOS Stream (the only distros where it's available). ensuregroup's usage of "(>=..., <=...)" constraints when evaluating the distro package, and "==" constraints when installing it from PyPI, makes it possible to avoid conflicts between the known-good version and a package plugins included in the distro. This is because package plugins have "==" constraints on the version that is included in the distro, and, using "pip install avocado==88.1" on a venv that includes system packages will result in an error: avocado-framework-plugin-varianter-yaml-to-mux 98.0 requires avocado-framework==98.0, but you have avocado-framework 88.1 which is incompatible. avocado-framework-plugin-result-html 98.0 requires avocado-framework==98.0, but you have avocado-framework 88.1 which is incompatible. But at the same time, if the venv does not include a system distribution of avocado then we can install a known-good version and stick to LTS releases. Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1663 Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
		
			
				
	
	
		
			1169 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1169 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
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
 | 
						|
    post_init
 | 
						|
              post-venv initialization
 | 
						|
    ensure    Ensure that the specified package is installed.
 | 
						|
    ensuregroup
 | 
						|
              Ensure that the specified package group is installed.
 | 
						|
 | 
						|
--------------------------------------------------
 | 
						|
 | 
						|
usage: mkvenv create [-h] target
 | 
						|
 | 
						|
positional arguments:
 | 
						|
  target      Target directory to install virtual environment into.
 | 
						|
 | 
						|
options:
 | 
						|
  -h, --help  show this help message and exit
 | 
						|
 | 
						|
--------------------------------------------------
 | 
						|
 | 
						|
usage: mkvenv post_init [-h]
 | 
						|
 | 
						|
options:
 | 
						|
  -h, --help         show this help message and exit
 | 
						|
 | 
						|
--------------------------------------------------
 | 
						|
 | 
						|
usage: mkvenv ensure [-h] [--online] [--dir DIR] dep_spec...
 | 
						|
 | 
						|
positional arguments:
 | 
						|
  dep_spec    PEP 508 Dependency specification, e.g. 'meson>=0.61.5'
 | 
						|
 | 
						|
options:
 | 
						|
  -h, --help  show this help message and exit
 | 
						|
  --online    Install packages from PyPI, if necessary.
 | 
						|
  --dir DIR   Path to vendored packages where we may install from.
 | 
						|
 | 
						|
--------------------------------------------------
 | 
						|
 | 
						|
usage: mkvenv ensuregroup [-h] [--online] [--dir DIR] file group...
 | 
						|
 | 
						|
positional arguments:
 | 
						|
  file        pointer to a TOML file
 | 
						|
  group       section name in the TOML file
 | 
						|
 | 
						|
options:
 | 
						|
  -h, --help  show this help message and exit
 | 
						|
  --online    Install packages from PyPI, if necessary.
 | 
						|
  --dir DIR   Path to vendored packages where we may install from.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
# The duplication between importlib and pkg_resources does not help
 | 
						|
# pylint: disable=too-many-lines
 | 
						|
 | 
						|
# 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 re
 | 
						|
import shutil
 | 
						|
import site
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import sysconfig
 | 
						|
from types import SimpleNamespace
 | 
						|
from typing import (
 | 
						|
    Any,
 | 
						|
    Dict,
 | 
						|
    Iterator,
 | 
						|
    Optional,
 | 
						|
    Sequence,
 | 
						|
    Tuple,
 | 
						|
    Union,
 | 
						|
)
 | 
						|
import venv
 | 
						|
 | 
						|
 | 
						|
# Try to load distlib, with a fallback to pip's vendored version.
 | 
						|
# HAVE_DISTLIB is checked below, just-in-time, so that mkvenv does not fail
 | 
						|
# outside the venv or before a potential call to ensurepip in checkpip().
 | 
						|
HAVE_DISTLIB = True
 | 
						|
try:
 | 
						|
    import distlib.scripts
 | 
						|
    import distlib.version
 | 
						|
except ImportError:
 | 
						|
    try:
 | 
						|
        # Reach into pip's cookie jar.  pylint and flake8 don't understand
 | 
						|
        # that these imports will be used via distlib.xxx.
 | 
						|
        from pip._vendor import distlib
 | 
						|
        import pip._vendor.distlib.scripts  # noqa, pylint: disable=unused-import
 | 
						|
        import pip._vendor.distlib.version  # noqa, pylint: disable=unused-import
 | 
						|
    except ImportError:
 | 
						|
        HAVE_DISTLIB = False
 | 
						|
 | 
						|
# Try to load tomllib, with a fallback to tomli.
 | 
						|
# HAVE_TOMLLIB is checked below, just-in-time, so that mkvenv does not fail
 | 
						|
# outside the venv or before a potential call to ensurepip in checkpip().
 | 
						|
HAVE_TOMLLIB = True
 | 
						|
try:
 | 
						|
    import tomllib
 | 
						|
except ImportError:
 | 
						|
    try:
 | 
						|
        import tomli as tomllib
 | 
						|
    except ImportError:
 | 
						|
        HAVE_TOMLLIB = False
 | 
						|
 | 
						|
# 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")
 | 
						|
 | 
						|
 | 
						|
def inside_a_venv() -> bool:
 | 
						|
    """Returns True if it is executed inside of a virtual environment."""
 | 
						|
    return sys.prefix != sys.base_prefix
 | 
						|
 | 
						|
 | 
						|
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.
 | 
						|
 | 
						|
    The primary difference is that it emulates a "nested" virtual
 | 
						|
    environment when invoked from inside of an existing virtual
 | 
						|
    environment by including packages from the parent.  Also,
 | 
						|
    "ensurepip" is replaced if possible with just recreating pip's
 | 
						|
    console_scripts inside the virtual environment.
 | 
						|
 | 
						|
    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__(...)")
 | 
						|
 | 
						|
        # For nested venv emulation:
 | 
						|
        self.use_parent_packages = False
 | 
						|
        if inside_a_venv():
 | 
						|
            # Include parent packages only if we're in a venv and
 | 
						|
            # system_site_packages was True.
 | 
						|
            self.use_parent_packages = kwargs.pop(
 | 
						|
                "system_site_packages", False
 | 
						|
            )
 | 
						|
            # Include system_site_packages only when the parent,
 | 
						|
            # The venv we are currently in, also does so.
 | 
						|
            kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES
 | 
						|
 | 
						|
        # ensurepip is slow: venv creation can be very fast for cases where
 | 
						|
        # we allow the use of system_site_packages. Therefore, ensurepip is
 | 
						|
        # replaced with our own script generation once the virtual environment
 | 
						|
        # is setup.
 | 
						|
        self.want_pip = kwargs.get("with_pip", False)
 | 
						|
        if self.want_pip:
 | 
						|
            if (
 | 
						|
                kwargs.get("system_site_packages", False)
 | 
						|
                and not need_ensurepip()
 | 
						|
            ):
 | 
						|
                kwargs["with_pip"] = False
 | 
						|
            else:
 | 
						|
                check_ensurepip(suggest_remedy=True)
 | 
						|
 | 
						|
        super().__init__(*args, **kwargs)
 | 
						|
 | 
						|
        # Make the context available post-creation:
 | 
						|
        self._context: Optional[SimpleNamespace] = None
 | 
						|
 | 
						|
    def get_parent_libpath(self) -> Optional[str]:
 | 
						|
        """Return the libpath of the parent venv, if applicable."""
 | 
						|
        if self.use_parent_packages:
 | 
						|
            return sysconfig.get_path("purelib")
 | 
						|
        return None
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def compute_venv_libpath(context: SimpleNamespace) -> str:
 | 
						|
        """
 | 
						|
        Compatibility wrapper for context.lib_path for Python < 3.12
 | 
						|
        """
 | 
						|
        # Python 3.12+, not strictly necessary because it's documented
 | 
						|
        # to be the same as 3.10 code below:
 | 
						|
        if sys.version_info >= (3, 12):
 | 
						|
            return context.lib_path
 | 
						|
 | 
						|
        # Python 3.10+
 | 
						|
        if "venv" in sysconfig.get_scheme_names():
 | 
						|
            lib_path = sysconfig.get_path(
 | 
						|
                "purelib", scheme="venv", vars={"base": context.env_dir}
 | 
						|
            )
 | 
						|
            assert lib_path is not None
 | 
						|
            return lib_path
 | 
						|
 | 
						|
        # For Python <= 3.9 we need to hardcode this. Fortunately the
 | 
						|
        # code below was the same in Python 3.6-3.10, so there is only
 | 
						|
        # one case.
 | 
						|
        if sys.platform == "win32":
 | 
						|
            return os.path.join(context.env_dir, "Lib", "site-packages")
 | 
						|
        return os.path.join(
 | 
						|
            context.env_dir,
 | 
						|
            "lib",
 | 
						|
            "python%d.%d" % sys.version_info[:2],
 | 
						|
            "site-packages",
 | 
						|
        )
 | 
						|
 | 
						|
    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 create(self, env_dir: DirType) -> None:
 | 
						|
        logger.debug("create(env_dir=%s)", env_dir)
 | 
						|
        super().create(env_dir)
 | 
						|
        assert self._context is not None
 | 
						|
        self.post_post_setup(self._context)
 | 
						|
 | 
						|
    def post_post_setup(self, context: SimpleNamespace) -> None:
 | 
						|
        """
 | 
						|
        The final, final hook. Enter the venv and run commands inside of it.
 | 
						|
        """
 | 
						|
        if self.use_parent_packages:
 | 
						|
            # We're inside of a venv and we want to include the parent
 | 
						|
            # venv's packages.
 | 
						|
            parent_libpath = self.get_parent_libpath()
 | 
						|
            assert parent_libpath is not None
 | 
						|
            logger.debug("parent_libpath: %s", parent_libpath)
 | 
						|
 | 
						|
            our_libpath = self.compute_venv_libpath(context)
 | 
						|
            logger.debug("our_libpath: %s", our_libpath)
 | 
						|
 | 
						|
            pth_file = os.path.join(our_libpath, "nested.pth")
 | 
						|
            with open(pth_file, "w", encoding="UTF-8") as file:
 | 
						|
                file.write(parent_libpath + os.linesep)
 | 
						|
 | 
						|
        if self.want_pip:
 | 
						|
            args = [
 | 
						|
                context.env_exe,
 | 
						|
                __file__,
 | 
						|
                "post_init",
 | 
						|
            ]
 | 
						|
            subprocess.run(args, check=True)
 | 
						|
 | 
						|
    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 need_ensurepip() -> bool:
 | 
						|
    """
 | 
						|
    Tests for the presence of setuptools and pip.
 | 
						|
 | 
						|
    :return: `True` if we do not detect both packages.
 | 
						|
    """
 | 
						|
    # Don't try to actually import them, it's fraught with danger:
 | 
						|
    # https://github.com/pypa/setuptools/issues/2993
 | 
						|
    if find_spec("setuptools") and find_spec("pip"):
 | 
						|
        return False
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def check_ensurepip(prefix: str = "", suggest_remedy: bool = False) -> 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"
 | 
						|
            "(Debian puts ensurepip in its python3-venv package.)\n"
 | 
						|
        )
 | 
						|
        if suggest_remedy:
 | 
						|
            msg += (
 | 
						|
                "Either install ensurepip, or alleviate the need for it in the"
 | 
						|
                " first place by installing pip and setuptools for "
 | 
						|
                f"'{sys.executable}'.\n"
 | 
						|
            )
 | 
						|
        raise Ouch(prefix + 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"
 | 
						|
            "(NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)\n"
 | 
						|
        )
 | 
						|
        if suggest_remedy:
 | 
						|
            msg += (
 | 
						|
                "Either install pyexpat, or alleviate the need for it in the "
 | 
						|
                "first place by installing pip and setuptools for "
 | 
						|
                f"'{sys.executable}'.\n"
 | 
						|
            )
 | 
						|
        raise Ouch(prefix + 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"
 | 
						|
    nested = ""
 | 
						|
    if builder.use_parent_packages:
 | 
						|
        nested = f"(with packages from '{builder.get_parent_libpath()}') "
 | 
						|
    print(
 | 
						|
        f"mkvenv: Creating {style} virtual environment"
 | 
						|
        f" {nested}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 _gen_importlib(packages: Sequence[str]) -> Iterator[str]:
 | 
						|
    # pylint: disable=import-outside-toplevel
 | 
						|
    # pylint: disable=no-name-in-module
 | 
						|
    # pylint: disable=import-error
 | 
						|
    try:
 | 
						|
        # First preference: Python 3.8+ stdlib
 | 
						|
        from importlib.metadata import (  # type: ignore
 | 
						|
            PackageNotFoundError,
 | 
						|
            distribution,
 | 
						|
        )
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
        # Second preference: Commonly available PyPI backport
 | 
						|
        from importlib_metadata import (  # type: ignore
 | 
						|
            PackageNotFoundError,
 | 
						|
            distribution,
 | 
						|
        )
 | 
						|
 | 
						|
    def _generator() -> Iterator[str]:
 | 
						|
        for package in packages:
 | 
						|
            try:
 | 
						|
                entry_points = distribution(package).entry_points
 | 
						|
            except PackageNotFoundError:
 | 
						|
                continue
 | 
						|
 | 
						|
            # The EntryPoints type is only available in 3.10+,
 | 
						|
            # treat this as a vanilla list and filter it ourselves.
 | 
						|
            entry_points = filter(
 | 
						|
                lambda ep: ep.group == "console_scripts", entry_points
 | 
						|
            )
 | 
						|
 | 
						|
            for entry_point in entry_points:
 | 
						|
                yield f"{entry_point.name} = {entry_point.value}"
 | 
						|
 | 
						|
    return _generator()
 | 
						|
 | 
						|
 | 
						|
def _gen_pkg_resources(packages: Sequence[str]) -> Iterator[str]:
 | 
						|
    # pylint: disable=import-outside-toplevel
 | 
						|
    # Bundled with setuptools; has a good chance of being available.
 | 
						|
    import pkg_resources
 | 
						|
 | 
						|
    def _generator() -> Iterator[str]:
 | 
						|
        for package in packages:
 | 
						|
            try:
 | 
						|
                eps = pkg_resources.get_entry_map(package, "console_scripts")
 | 
						|
            except pkg_resources.DistributionNotFound:
 | 
						|
                continue
 | 
						|
 | 
						|
            for entry_point in eps.values():
 | 
						|
                yield str(entry_point)
 | 
						|
 | 
						|
    return _generator()
 | 
						|
 | 
						|
 | 
						|
def generate_console_scripts(
 | 
						|
    packages: Sequence[str],
 | 
						|
    python_path: Optional[str] = None,
 | 
						|
    bin_path: Optional[str] = None,
 | 
						|
) -> None:
 | 
						|
    """
 | 
						|
    Generate script shims for console_script entry points in @packages.
 | 
						|
    """
 | 
						|
    if python_path is None:
 | 
						|
        python_path = sys.executable
 | 
						|
    if bin_path is None:
 | 
						|
        bin_path = sysconfig.get_path("scripts")
 | 
						|
        assert bin_path is not None
 | 
						|
 | 
						|
    logger.debug(
 | 
						|
        "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)",
 | 
						|
        packages,
 | 
						|
        python_path,
 | 
						|
        bin_path,
 | 
						|
    )
 | 
						|
 | 
						|
    if not packages:
 | 
						|
        return
 | 
						|
 | 
						|
    def _get_entry_points() -> Iterator[str]:
 | 
						|
        """Python 3.7 compatibility shim for iterating entry points."""
 | 
						|
        # Python 3.8+, or Python 3.7 with importlib_metadata installed.
 | 
						|
        try:
 | 
						|
            return _gen_importlib(packages)
 | 
						|
        except ImportError as exc:
 | 
						|
            logger.debug("%s", str(exc))
 | 
						|
 | 
						|
        # Python 3.7 with setuptools installed.
 | 
						|
        try:
 | 
						|
            return _gen_pkg_resources(packages)
 | 
						|
        except ImportError as exc:
 | 
						|
            logger.debug("%s", str(exc))
 | 
						|
            raise Ouch(
 | 
						|
                "Neither importlib.metadata nor pkg_resources found, "
 | 
						|
                "can't generate console script shims.\n"
 | 
						|
                "Use Python 3.8+, or install importlib-metadata or setuptools."
 | 
						|
            ) from exc
 | 
						|
 | 
						|
    maker = distlib.scripts.ScriptMaker(None, bin_path)
 | 
						|
    maker.variants = {""}
 | 
						|
    maker.clobber = False
 | 
						|
 | 
						|
    for entry_point in _get_entry_points():
 | 
						|
        for filename in maker.make(entry_point):
 | 
						|
            logger.debug("wrote console_script '%s'", filename)
 | 
						|
 | 
						|
 | 
						|
def checkpip() -> bool:
 | 
						|
    """
 | 
						|
    Debian10 has a pip that's broken when used inside of a virtual environment.
 | 
						|
 | 
						|
    We try to detect and correct that case here.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        # pylint: disable=import-outside-toplevel,unused-import,import-error
 | 
						|
        # pylint: disable=redefined-outer-name
 | 
						|
        import pip._internal  # type: ignore  # noqa: F401
 | 
						|
 | 
						|
        logger.debug("pip appears to be working correctly.")
 | 
						|
        return False
 | 
						|
    except ModuleNotFoundError as exc:
 | 
						|
        if exc.name == "pip._internal":
 | 
						|
            # Uh, fair enough. They did say "internal".
 | 
						|
            # Let's just assume it's fine.
 | 
						|
            return False
 | 
						|
        logger.warning("pip appears to be malfunctioning: %s", str(exc))
 | 
						|
 | 
						|
    check_ensurepip("pip appears to be non-functional, and ")
 | 
						|
 | 
						|
    logger.debug("Attempting to repair pip ...")
 | 
						|
    subprocess.run(
 | 
						|
        (sys.executable, "-m", "ensurepip"),
 | 
						|
        stdout=subprocess.DEVNULL,
 | 
						|
        check=True,
 | 
						|
    )
 | 
						|
    logger.debug("Pip is now (hopefully) repaired!")
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def pkgname_from_depspec(dep_spec: str) -> str:
 | 
						|
    """
 | 
						|
    Parse package name out of a PEP-508 depspec.
 | 
						|
 | 
						|
    See https://peps.python.org/pep-0508/#names
 | 
						|
    """
 | 
						|
    match = re.match(
 | 
						|
        r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE
 | 
						|
    )
 | 
						|
    if not match:
 | 
						|
        raise ValueError(
 | 
						|
            f"dep_spec '{dep_spec}'"
 | 
						|
            " does not appear to contain a valid package name"
 | 
						|
        )
 | 
						|
    return match.group(0)
 | 
						|
 | 
						|
 | 
						|
def _get_path_importlib(package: str) -> Optional[str]:
 | 
						|
    # pylint: disable=import-outside-toplevel
 | 
						|
    # pylint: disable=no-name-in-module
 | 
						|
    # pylint: disable=import-error
 | 
						|
    try:
 | 
						|
        # First preference: Python 3.8+ stdlib
 | 
						|
        from importlib.metadata import (  # type: ignore
 | 
						|
            PackageNotFoundError,
 | 
						|
            distribution,
 | 
						|
        )
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
        # Second preference: Commonly available PyPI backport
 | 
						|
        from importlib_metadata import (  # type: ignore
 | 
						|
            PackageNotFoundError,
 | 
						|
            distribution,
 | 
						|
        )
 | 
						|
 | 
						|
    try:
 | 
						|
        return str(distribution(package).locate_file("."))
 | 
						|
    except PackageNotFoundError:
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def _get_path_pkg_resources(package: str) -> Optional[str]:
 | 
						|
    # pylint: disable=import-outside-toplevel
 | 
						|
    # Bundled with setuptools; has a good chance of being available.
 | 
						|
    import pkg_resources
 | 
						|
 | 
						|
    try:
 | 
						|
        return str(pkg_resources.get_distribution(package).location)
 | 
						|
    except pkg_resources.DistributionNotFound:
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def _get_path(package: str) -> Optional[str]:
 | 
						|
    try:
 | 
						|
        return _get_path_importlib(package)
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
 | 
						|
    try:
 | 
						|
        return _get_path_pkg_resources(package)
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
        raise Ouch(
 | 
						|
            "Neither importlib.metadata nor pkg_resources found. "
 | 
						|
            "Use Python 3.8+, or install importlib-metadata or setuptools."
 | 
						|
        ) from exc
 | 
						|
 | 
						|
 | 
						|
def _path_is_prefix(prefix: Optional[str], path: str) -> bool:
 | 
						|
    try:
 | 
						|
        return (
 | 
						|
            prefix is not None and os.path.commonpath([prefix, path]) == prefix
 | 
						|
        )
 | 
						|
    except ValueError:
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
def _is_system_package(package: str) -> bool:
 | 
						|
    path = _get_path(package)
 | 
						|
    return path is not None and not (
 | 
						|
        _path_is_prefix(sysconfig.get_path("purelib"), path)
 | 
						|
        or _path_is_prefix(sysconfig.get_path("platlib"), path)
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def _get_version_importlib(package: str) -> Optional[str]:
 | 
						|
    # pylint: disable=import-outside-toplevel
 | 
						|
    # pylint: disable=no-name-in-module
 | 
						|
    # pylint: disable=import-error
 | 
						|
    try:
 | 
						|
        # First preference: Python 3.8+ stdlib
 | 
						|
        from importlib.metadata import (  # type: ignore
 | 
						|
            PackageNotFoundError,
 | 
						|
            distribution,
 | 
						|
        )
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
        # Second preference: Commonly available PyPI backport
 | 
						|
        from importlib_metadata import (  # type: ignore
 | 
						|
            PackageNotFoundError,
 | 
						|
            distribution,
 | 
						|
        )
 | 
						|
 | 
						|
    try:
 | 
						|
        return str(distribution(package).version)
 | 
						|
    except PackageNotFoundError:
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def _get_version_pkg_resources(package: str) -> Optional[str]:
 | 
						|
    # pylint: disable=import-outside-toplevel
 | 
						|
    # Bundled with setuptools; has a good chance of being available.
 | 
						|
    import pkg_resources
 | 
						|
 | 
						|
    try:
 | 
						|
        return str(pkg_resources.get_distribution(package).version)
 | 
						|
    except pkg_resources.DistributionNotFound:
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def _get_version(package: str) -> Optional[str]:
 | 
						|
    try:
 | 
						|
        return _get_version_importlib(package)
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
 | 
						|
    try:
 | 
						|
        return _get_version_pkg_resources(package)
 | 
						|
    except ImportError as exc:
 | 
						|
        logger.debug("%s", str(exc))
 | 
						|
        raise Ouch(
 | 
						|
            "Neither importlib.metadata nor pkg_resources found. "
 | 
						|
            "Use Python 3.8+, or install importlib-metadata or setuptools."
 | 
						|
        ) from exc
 | 
						|
 | 
						|
 | 
						|
def diagnose(
 | 
						|
    dep_spec: str,
 | 
						|
    online: bool,
 | 
						|
    wheels_dir: Optional[Union[str, Path]],
 | 
						|
    prog: Optional[str],
 | 
						|
) -> Tuple[str, bool]:
 | 
						|
    """
 | 
						|
    Offer a summary to the user as to why a package failed to be installed.
 | 
						|
 | 
						|
    :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5'
 | 
						|
    :param online: Did we allow PyPI access?
 | 
						|
    :param prog:
 | 
						|
        Optionally, a shell program name that can be used as a
 | 
						|
        bellwether to detect if this program is installed elsewhere on
 | 
						|
        the system. This is used to offer advice when a program is
 | 
						|
        detected for a different python version.
 | 
						|
    :param wheels_dir:
 | 
						|
        Optionally, a directory that was searched for vendored packages.
 | 
						|
    """
 | 
						|
    # pylint: disable=too-many-branches
 | 
						|
 | 
						|
    # Some errors are not particularly serious
 | 
						|
    bad = False
 | 
						|
 | 
						|
    pkg_name = pkgname_from_depspec(dep_spec)
 | 
						|
    pkg_version = _get_version(pkg_name)
 | 
						|
 | 
						|
    lines = []
 | 
						|
 | 
						|
    if pkg_version:
 | 
						|
        lines.append(
 | 
						|
            f"Python package '{pkg_name}' version '{pkg_version}' was found,"
 | 
						|
            " but isn't suitable."
 | 
						|
        )
 | 
						|
    else:
 | 
						|
        lines.append(
 | 
						|
            f"Python package '{pkg_name}' was not found nor installed."
 | 
						|
        )
 | 
						|
 | 
						|
    if wheels_dir:
 | 
						|
        lines.append(
 | 
						|
            "No suitable version found in, or failed to install from"
 | 
						|
            f" '{wheels_dir}'."
 | 
						|
        )
 | 
						|
        bad = True
 | 
						|
 | 
						|
    if online:
 | 
						|
        lines.append("A suitable version could not be obtained from PyPI.")
 | 
						|
        bad = True
 | 
						|
    else:
 | 
						|
        lines.append(
 | 
						|
            "mkvenv was configured to operate offline and did not check PyPI."
 | 
						|
        )
 | 
						|
 | 
						|
    if prog and not pkg_version:
 | 
						|
        which = shutil.which(prog)
 | 
						|
        if which:
 | 
						|
            if sys.base_prefix in site.PREFIXES:
 | 
						|
                pypath = Path(sys.executable).resolve()
 | 
						|
                lines.append(
 | 
						|
                    f"'{prog}' was detected on your system at '{which}', "
 | 
						|
                    f"but the Python package '{pkg_name}' was not found by "
 | 
						|
                    f"this Python interpreter ('{pypath}'). "
 | 
						|
                    f"Typically this means that '{prog}' has been installed "
 | 
						|
                    "against a different Python interpreter on your system."
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                lines.append(
 | 
						|
                    f"'{prog}' was detected on your system at '{which}', "
 | 
						|
                    "but the build is using an isolated virtual environment."
 | 
						|
                )
 | 
						|
            bad = True
 | 
						|
 | 
						|
    lines = [f" • {line}" for line in lines]
 | 
						|
    if bad:
 | 
						|
        lines.insert(0, f"Could not provide build dependency '{dep_spec}':")
 | 
						|
    else:
 | 
						|
        lines.insert(0, f"'{dep_spec}' not found:")
 | 
						|
    return os.linesep.join(lines), bad
 | 
						|
 | 
						|
 | 
						|
def pip_install(
 | 
						|
    args: Sequence[str],
 | 
						|
    online: bool = False,
 | 
						|
    wheels_dir: Optional[Union[str, Path]] = None,
 | 
						|
) -> None:
 | 
						|
    """
 | 
						|
    Use pip to install a package or package(s) as specified in @args.
 | 
						|
    """
 | 
						|
    loud = bool(
 | 
						|
        os.environ.get("DEBUG")
 | 
						|
        or os.environ.get("GITLAB_CI")
 | 
						|
        or os.environ.get("V")
 | 
						|
    )
 | 
						|
 | 
						|
    full_args = [
 | 
						|
        sys.executable,
 | 
						|
        "-m",
 | 
						|
        "pip",
 | 
						|
        "install",
 | 
						|
        "--disable-pip-version-check",
 | 
						|
        "-v" if loud else "-q",
 | 
						|
    ]
 | 
						|
    if not online:
 | 
						|
        full_args += ["--no-index"]
 | 
						|
    if wheels_dir:
 | 
						|
        full_args += ["--find-links", f"file://{str(wheels_dir)}"]
 | 
						|
    full_args += list(args)
 | 
						|
    subprocess.run(
 | 
						|
        full_args,
 | 
						|
        check=True,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def _make_version_constraint(info: Dict[str, str], install: bool) -> str:
 | 
						|
    """
 | 
						|
    Construct the version constraint part of a PEP 508 dependency
 | 
						|
    specification (for example '>=0.61.5') from the accepted and
 | 
						|
    installed keys of the provided dictionary.
 | 
						|
 | 
						|
    :param info: A dictionary corresponding to a TOML key-value list.
 | 
						|
    :param install: True generates install constraints, False generates
 | 
						|
        presence constraints
 | 
						|
    """
 | 
						|
    if install and "installed" in info:
 | 
						|
        return "==" + info["installed"]
 | 
						|
 | 
						|
    dep_spec = info.get("accepted", "")
 | 
						|
    dep_spec = dep_spec.strip()
 | 
						|
    # Double check that they didn't just use a version number
 | 
						|
    if dep_spec and dep_spec[0] not in "!~><=(":
 | 
						|
        raise Ouch(
 | 
						|
            "invalid dependency specifier " + dep_spec + " in dependency file"
 | 
						|
        )
 | 
						|
 | 
						|
    return dep_spec
 | 
						|
 | 
						|
 | 
						|
def _do_ensure(
 | 
						|
    group: Dict[str, Dict[str, str]],
 | 
						|
    online: bool = False,
 | 
						|
    wheels_dir: Optional[Union[str, Path]] = None,
 | 
						|
) -> Optional[Tuple[str, bool]]:
 | 
						|
    """
 | 
						|
    Use pip to ensure we have the packages specified in @group.
 | 
						|
 | 
						|
    If the packages are already installed, do nothing. If online and
 | 
						|
    wheels_dir are both provided, prefer packages found in wheels_dir
 | 
						|
    first before connecting to PyPI.
 | 
						|
 | 
						|
    :param group: A dictionary of dictionaries, corresponding to a
 | 
						|
        section in a pythondeps.toml file.
 | 
						|
    :param online: If True, fall back to PyPI.
 | 
						|
    :param wheels_dir: If specified, search this path for packages.
 | 
						|
    """
 | 
						|
    absent = []
 | 
						|
    present = []
 | 
						|
    canary = None
 | 
						|
    for name, info in group.items():
 | 
						|
        constraint = _make_version_constraint(info, False)
 | 
						|
        matcher = distlib.version.LegacyMatcher(name + constraint)
 | 
						|
        print(f"mkvenv: checking for {matcher}", file=sys.stderr)
 | 
						|
        ver = _get_version(name)
 | 
						|
        if (
 | 
						|
            ver is None
 | 
						|
            # Always pass installed package to pip, so that they can be
 | 
						|
            # updated if the requested version changes
 | 
						|
            or not _is_system_package(name)
 | 
						|
            or not matcher.match(distlib.version.LegacyVersion(ver))
 | 
						|
        ):
 | 
						|
            absent.append(name + _make_version_constraint(info, True))
 | 
						|
            if len(absent) == 1:
 | 
						|
                canary = info.get("canary", None)
 | 
						|
        else:
 | 
						|
            logger.info("found %s %s", name, ver)
 | 
						|
            present.append(name)
 | 
						|
 | 
						|
    if present:
 | 
						|
        generate_console_scripts(present)
 | 
						|
 | 
						|
    if absent:
 | 
						|
        if online or wheels_dir:
 | 
						|
            # Some packages are missing or aren't a suitable version,
 | 
						|
            # install a suitable (possibly vendored) package.
 | 
						|
            print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr)
 | 
						|
            try:
 | 
						|
                pip_install(args=absent, online=online, wheels_dir=wheels_dir)
 | 
						|
                return None
 | 
						|
            except subprocess.CalledProcessError:
 | 
						|
                pass
 | 
						|
 | 
						|
        return diagnose(
 | 
						|
            absent[0],
 | 
						|
            online,
 | 
						|
            wheels_dir,
 | 
						|
            canary,
 | 
						|
        )
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
def ensure(
 | 
						|
    dep_specs: Sequence[str],
 | 
						|
    online: bool = False,
 | 
						|
    wheels_dir: Optional[Union[str, Path]] = None,
 | 
						|
    prog: Optional[str] = None,
 | 
						|
) -> None:
 | 
						|
    """
 | 
						|
    Use pip to ensure we have the package specified by @dep_specs.
 | 
						|
 | 
						|
    If the package is already installed, do nothing. If online and
 | 
						|
    wheels_dir are both provided, prefer packages found in wheels_dir
 | 
						|
    first before connecting to PyPI.
 | 
						|
 | 
						|
    :param dep_specs:
 | 
						|
        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
 | 
						|
    :param online: If True, fall back to PyPI.
 | 
						|
    :param wheels_dir: If specified, search this path for packages.
 | 
						|
    :param prog:
 | 
						|
        If specified, use this program name for error diagnostics that will
 | 
						|
        be presented to the user. e.g., 'sphinx-build' can be used as a
 | 
						|
        bellwether for the presence of 'sphinx'.
 | 
						|
    """
 | 
						|
 | 
						|
    if not HAVE_DISTLIB:
 | 
						|
        raise Ouch("a usable distlib could not be found, please install it")
 | 
						|
 | 
						|
    # Convert the depspecs to a dictionary, as if they came
 | 
						|
    # from a section in a pythondeps.toml file
 | 
						|
    group: Dict[str, Dict[str, str]] = {}
 | 
						|
    for spec in dep_specs:
 | 
						|
        name = distlib.version.LegacyMatcher(spec).name
 | 
						|
        group[name] = {}
 | 
						|
 | 
						|
        spec = spec.strip()
 | 
						|
        pos = len(name)
 | 
						|
        ver = spec[pos:].strip()
 | 
						|
        if ver:
 | 
						|
            group[name]["accepted"] = ver
 | 
						|
 | 
						|
        if prog:
 | 
						|
            group[name]["canary"] = prog
 | 
						|
            prog = None
 | 
						|
 | 
						|
    result = _do_ensure(group, online, wheels_dir)
 | 
						|
    if result:
 | 
						|
        # Well, that's not good.
 | 
						|
        if result[1]:
 | 
						|
            raise Ouch(result[0])
 | 
						|
        raise SystemExit(f"\n{result[0]}\n\n")
 | 
						|
 | 
						|
 | 
						|
def _parse_groups(file: str) -> Dict[str, Dict[str, Any]]:
 | 
						|
    if not HAVE_TOMLLIB:
 | 
						|
        if sys.version_info < (3, 11):
 | 
						|
            raise Ouch("found no usable tomli, please install it")
 | 
						|
 | 
						|
        raise Ouch(
 | 
						|
            "Python >=3.11 does not have tomllib... what have you done!?"
 | 
						|
        )
 | 
						|
 | 
						|
    # Use loads() to support both tomli v1.2.x (Ubuntu 22.04,
 | 
						|
    # Debian bullseye-backports) and v2.0.x
 | 
						|
    with open(file, "r", encoding="ascii") as depfile:
 | 
						|
        contents = depfile.read()
 | 
						|
        return tomllib.loads(contents)  # type: ignore
 | 
						|
 | 
						|
 | 
						|
def ensure_group(
 | 
						|
    file: str,
 | 
						|
    groups: Sequence[str],
 | 
						|
    online: bool = False,
 | 
						|
    wheels_dir: Optional[Union[str, Path]] = None,
 | 
						|
) -> None:
 | 
						|
    """
 | 
						|
    Use pip to ensure we have the package specified by @dep_specs.
 | 
						|
 | 
						|
    If the package is already installed, do nothing. If online and
 | 
						|
    wheels_dir are both provided, prefer packages found in wheels_dir
 | 
						|
    first before connecting to PyPI.
 | 
						|
 | 
						|
    :param dep_specs:
 | 
						|
        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
 | 
						|
    :param online: If True, fall back to PyPI.
 | 
						|
    :param wheels_dir: If specified, search this path for packages.
 | 
						|
    """
 | 
						|
 | 
						|
    if not HAVE_DISTLIB:
 | 
						|
        raise Ouch("found no usable distlib, please install it")
 | 
						|
 | 
						|
    parsed_deps = _parse_groups(file)
 | 
						|
 | 
						|
    to_install: Dict[str, Dict[str, str]] = {}
 | 
						|
    for group in groups:
 | 
						|
        try:
 | 
						|
            to_install.update(parsed_deps[group])
 | 
						|
        except KeyError as exc:
 | 
						|
            raise Ouch(f"group {group} not defined") from exc
 | 
						|
 | 
						|
    result = _do_ensure(to_install, online, wheels_dir)
 | 
						|
    if result:
 | 
						|
        # Well, that's not good.
 | 
						|
        if result[1]:
 | 
						|
            raise Ouch(result[0])
 | 
						|
        raise SystemExit(f"\n{result[0]}\n\n")
 | 
						|
 | 
						|
 | 
						|
def post_venv_setup() -> None:
 | 
						|
    """
 | 
						|
    This is intended to be run *inside the venv* after it is created.
 | 
						|
    """
 | 
						|
    logger.debug("post_venv_setup()")
 | 
						|
    # Test for a broken pip (Debian 10 or derivative?) and fix it if needed
 | 
						|
    if not checkpip():
 | 
						|
        # Finally, generate a 'pip' script so the venv is usable in a normal
 | 
						|
        # way from the CLI. This only happens when we inherited pip from a
 | 
						|
        # parent/system-site and haven't run ensurepip in some way.
 | 
						|
        generate_console_scripts(["pip"])
 | 
						|
 | 
						|
 | 
						|
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 _add_post_init_subcommand(subparsers: Any) -> None:
 | 
						|
    subparsers.add_parser("post_init", help="post-venv initialization")
 | 
						|
 | 
						|
 | 
						|
def _add_ensuregroup_subcommand(subparsers: Any) -> None:
 | 
						|
    subparser = subparsers.add_parser(
 | 
						|
        "ensuregroup",
 | 
						|
        help="Ensure that the specified package group is installed.",
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "--online",
 | 
						|
        action="store_true",
 | 
						|
        help="Install packages from PyPI, if necessary.",
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "--dir",
 | 
						|
        type=str,
 | 
						|
        action="store",
 | 
						|
        help="Path to vendored packages where we may install from.",
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "file",
 | 
						|
        type=str,
 | 
						|
        action="store",
 | 
						|
        help=("Path to a TOML file describing package groups"),
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "group",
 | 
						|
        type=str,
 | 
						|
        action="store",
 | 
						|
        help="One or more package group names",
 | 
						|
        nargs="+",
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def _add_ensure_subcommand(subparsers: Any) -> None:
 | 
						|
    subparser = subparsers.add_parser(
 | 
						|
        "ensure", help="Ensure that the specified package is installed."
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "--online",
 | 
						|
        action="store_true",
 | 
						|
        help="Install packages from PyPI, if necessary.",
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "--dir",
 | 
						|
        type=str,
 | 
						|
        action="store",
 | 
						|
        help="Path to vendored packages where we may install from.",
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "--diagnose",
 | 
						|
        type=str,
 | 
						|
        action="store",
 | 
						|
        help=(
 | 
						|
            "Name of a shell utility to use for "
 | 
						|
            "diagnostics if this command fails."
 | 
						|
        ),
 | 
						|
    )
 | 
						|
    subparser.add_argument(
 | 
						|
        "dep_specs",
 | 
						|
        type=str,
 | 
						|
        action="store",
 | 
						|
        help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'",
 | 
						|
        nargs="+",
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
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)
 | 
						|
    else:
 | 
						|
        if 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",
 | 
						|
        required=True,
 | 
						|
        metavar="command",
 | 
						|
        help="Description",
 | 
						|
    )
 | 
						|
 | 
						|
    _add_create_subcommand(subparsers)
 | 
						|
    _add_post_init_subcommand(subparsers)
 | 
						|
    _add_ensure_subcommand(subparsers)
 | 
						|
    _add_ensuregroup_subcommand(subparsers)
 | 
						|
 | 
						|
    args = parser.parse_args()
 | 
						|
    try:
 | 
						|
        if args.command == "create":
 | 
						|
            make_venv(
 | 
						|
                args.target,
 | 
						|
                system_site_packages=True,
 | 
						|
                clear=True,
 | 
						|
            )
 | 
						|
        if args.command == "post_init":
 | 
						|
            post_venv_setup()
 | 
						|
        if args.command == "ensure":
 | 
						|
            ensure(
 | 
						|
                dep_specs=args.dep_specs,
 | 
						|
                online=args.online,
 | 
						|
                wheels_dir=args.dir,
 | 
						|
                prog=args.diagnose,
 | 
						|
            )
 | 
						|
        if args.command == "ensuregroup":
 | 
						|
            ensure_group(
 | 
						|
                file=args.file,
 | 
						|
                groups=args.group,
 | 
						|
                online=args.online,
 | 
						|
                wheels_dir=args.dir,
 | 
						|
            )
 | 
						|
        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())
 |