xref: /qemu/python/scripts/mkvenv.py (revision bd5629db935a6c17c86ffbb6a39aa85eed807346)
1dd84028fSJohn Snow"""
2dd84028fSJohn Snowmkvenv - QEMU pyvenv bootstrapping utility
3dd84028fSJohn Snow
4dd84028fSJohn Snowusage: mkvenv [-h] command ...
5dd84028fSJohn Snow
6dd84028fSJohn SnowQEMU pyvenv bootstrapping utility
7dd84028fSJohn Snow
8dd84028fSJohn Snowoptions:
9dd84028fSJohn Snow  -h, --help  show this help message and exit
10dd84028fSJohn Snow
11dd84028fSJohn SnowCommands:
12dd84028fSJohn Snow  command     Description
13dd84028fSJohn Snow    create    create a venv
14f1ad527fSJohn Snow    post_init
15f1ad527fSJohn Snow              post-venv initialization
1671ed611cSPaolo Bonzini    ensuregroup
1771ed611cSPaolo Bonzini              Ensure that the specified package group is installed.
18dd84028fSJohn Snow
19dd84028fSJohn Snow--------------------------------------------------
20dd84028fSJohn Snow
21dd84028fSJohn Snowusage: mkvenv create [-h] target
22dd84028fSJohn Snow
23dd84028fSJohn Snowpositional arguments:
24dd84028fSJohn Snow  target      Target directory to install virtual environment into.
25dd84028fSJohn Snow
26dd84028fSJohn Snowoptions:
27dd84028fSJohn Snow  -h, --help  show this help message and exit
28dd84028fSJohn Snow
29c5538eedSJohn Snow--------------------------------------------------
30c5538eedSJohn Snow
31f1ad527fSJohn Snowusage: mkvenv post_init [-h]
32f1ad527fSJohn Snow
33f1ad527fSJohn Snowoptions:
34f1ad527fSJohn Snow  -h, --help         show this help message and exit
35f1ad527fSJohn Snow
36f1ad527fSJohn Snow--------------------------------------------------
37f1ad527fSJohn Snow
3871ed611cSPaolo Bonziniusage: mkvenv ensuregroup [-h] [--online] [--dir DIR] file group...
3971ed611cSPaolo Bonzini
4071ed611cSPaolo Bonzinipositional arguments:
4171ed611cSPaolo Bonzini  file        pointer to a TOML file
4271ed611cSPaolo Bonzini  group       section name in the TOML file
4371ed611cSPaolo Bonzini
4471ed611cSPaolo Bonzinioptions:
4571ed611cSPaolo Bonzini  -h, --help  show this help message and exit
4671ed611cSPaolo Bonzini  --online    Install packages from PyPI, if necessary.
4771ed611cSPaolo Bonzini  --dir DIR   Path to vendored packages where we may install from.
4871ed611cSPaolo Bonzini
49dd84028fSJohn Snow"""
50dd84028fSJohn Snow
51dd84028fSJohn Snow# Copyright (C) 2022-2023 Red Hat, Inc.
52dd84028fSJohn Snow#
53dd84028fSJohn Snow# Authors:
54dd84028fSJohn Snow#  John Snow <jsnow@redhat.com>
55dd84028fSJohn Snow#  Paolo Bonzini <pbonzini@redhat.com>
56dd84028fSJohn Snow#
57dd84028fSJohn Snow# This work is licensed under the terms of the GNU GPL, version 2 or
58dd84028fSJohn Snow# later. See the COPYING file in the top-level directory.
59dd84028fSJohn Snow
60dd84028fSJohn Snowimport argparse
613e4b6b0aSPaolo Bonzinifrom importlib.metadata import (
623e4b6b0aSPaolo Bonzini    Distribution,
633e4b6b0aSPaolo Bonzini    EntryPoint,
643e4b6b0aSPaolo Bonzini    PackageNotFoundError,
653e4b6b0aSPaolo Bonzini    distribution,
663e4b6b0aSPaolo Bonzini    version,
673e4b6b0aSPaolo Bonzini)
68a9dbde71SJohn Snowfrom importlib.util import find_spec
69dd84028fSJohn Snowimport logging
70dd84028fSJohn Snowimport os
71dd84028fSJohn Snowfrom pathlib import Path
724695a22eSJohn Snowimport re
734695a22eSJohn Snowimport shutil
74dee01b82SJohn Snowimport site
75dd84028fSJohn Snowimport subprocess
76dd84028fSJohn Snowimport sys
77dee01b82SJohn Snowimport sysconfig
78dd84028fSJohn Snowfrom types import SimpleNamespace
79c5538eedSJohn Snowfrom typing import (
80c5538eedSJohn Snow    Any,
810f1ec070SPaolo Bonzini    Dict,
8292834894SJohn Snow    Iterator,
83c5538eedSJohn Snow    Optional,
84c5538eedSJohn Snow    Sequence,
854695a22eSJohn Snow    Tuple,
86c5538eedSJohn Snow    Union,
87c5538eedSJohn Snow)
88dd84028fSJohn Snowimport venv
89c5538eedSJohn Snow
9068ea6d17SJohn Snow
9168ea6d17SJohn Snow# Try to load distlib, with a fallback to pip's vendored version.
9268ea6d17SJohn Snow# HAVE_DISTLIB is checked below, just-in-time, so that mkvenv does not fail
9368ea6d17SJohn Snow# outside the venv or before a potential call to ensurepip in checkpip().
9468ea6d17SJohn SnowHAVE_DISTLIB = True
9568ea6d17SJohn Snowtry:
9692834894SJohn Snow    import distlib.scripts
97c5538eedSJohn Snow    import distlib.version
9868ea6d17SJohn Snowexcept ImportError:
9968ea6d17SJohn Snow    try:
10068ea6d17SJohn Snow        # Reach into pip's cookie jar.  pylint and flake8 don't understand
10168ea6d17SJohn Snow        # that these imports will be used via distlib.xxx.
10268ea6d17SJohn Snow        from pip._vendor import distlib
10368ea6d17SJohn Snow        import pip._vendor.distlib.scripts  # noqa, pylint: disable=unused-import
10468ea6d17SJohn Snow        import pip._vendor.distlib.version  # noqa, pylint: disable=unused-import
10568ea6d17SJohn Snow    except ImportError:
10668ea6d17SJohn Snow        HAVE_DISTLIB = False
107dd84028fSJohn Snow
10871ed611cSPaolo Bonzini# Try to load tomllib, with a fallback to tomli.
10971ed611cSPaolo Bonzini# HAVE_TOMLLIB is checked below, just-in-time, so that mkvenv does not fail
11071ed611cSPaolo Bonzini# outside the venv or before a potential call to ensurepip in checkpip().
11171ed611cSPaolo BonziniHAVE_TOMLLIB = True
11271ed611cSPaolo Bonzinitry:
11371ed611cSPaolo Bonzini    import tomllib
11471ed611cSPaolo Bonziniexcept ImportError:
11571ed611cSPaolo Bonzini    try:
11671ed611cSPaolo Bonzini        import tomli as tomllib
11771ed611cSPaolo Bonzini    except ImportError:
11871ed611cSPaolo Bonzini        HAVE_TOMLLIB = False
11971ed611cSPaolo Bonzini
120dd84028fSJohn Snow# Do not add any mandatory dependencies from outside the stdlib:
121dd84028fSJohn Snow# This script *must* be usable standalone!
122dd84028fSJohn Snow
123dd84028fSJohn SnowDirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
124dd84028fSJohn Snowlogger = logging.getLogger("mkvenv")
125dd84028fSJohn Snow
126dd84028fSJohn Snow
127dee01b82SJohn Snowdef inside_a_venv() -> bool:
128dee01b82SJohn Snow    """Returns True if it is executed inside of a virtual environment."""
129dee01b82SJohn Snow    return sys.prefix != sys.base_prefix
130dee01b82SJohn Snow
131dee01b82SJohn Snow
132dd84028fSJohn Snowclass Ouch(RuntimeError):
133dd84028fSJohn Snow    """An Exception class we can't confuse with a builtin."""
134dd84028fSJohn Snow
135dd84028fSJohn Snow
136dd84028fSJohn Snowclass QemuEnvBuilder(venv.EnvBuilder):
137dd84028fSJohn Snow    """
138dd84028fSJohn Snow    An extension of venv.EnvBuilder for building QEMU's configure-time venv.
139dd84028fSJohn Snow
140dee01b82SJohn Snow    The primary difference is that it emulates a "nested" virtual
141dee01b82SJohn Snow    environment when invoked from inside of an existing virtual
142f1ad527fSJohn Snow    environment by including packages from the parent.  Also,
143f1ad527fSJohn Snow    "ensurepip" is replaced if possible with just recreating pip's
144f1ad527fSJohn Snow    console_scripts inside the virtual environment.
145dd84028fSJohn Snow
146dd84028fSJohn Snow    Parameters for base class init:
147dd84028fSJohn Snow      - system_site_packages: bool = False
148dd84028fSJohn Snow      - clear: bool = False
149dd84028fSJohn Snow      - symlinks: bool = False
150dd84028fSJohn Snow      - upgrade: bool = False
151dd84028fSJohn Snow      - with_pip: bool = False
152dd84028fSJohn Snow      - prompt: Optional[str] = None
153dd84028fSJohn Snow      - upgrade_deps: bool = False             (Since 3.9)
154dd84028fSJohn Snow    """
155dd84028fSJohn Snow
156dd84028fSJohn Snow    def __init__(self, *args: Any, **kwargs: Any) -> None:
157dd84028fSJohn Snow        logger.debug("QemuEnvBuilder.__init__(...)")
158a9dbde71SJohn Snow
159dee01b82SJohn Snow        # For nested venv emulation:
160dee01b82SJohn Snow        self.use_parent_packages = False
161dee01b82SJohn Snow        if inside_a_venv():
162dee01b82SJohn Snow            # Include parent packages only if we're in a venv and
163dee01b82SJohn Snow            # system_site_packages was True.
164dee01b82SJohn Snow            self.use_parent_packages = kwargs.pop(
165dee01b82SJohn Snow                "system_site_packages", False
166dee01b82SJohn Snow            )
167dee01b82SJohn Snow            # Include system_site_packages only when the parent,
168dee01b82SJohn Snow            # The venv we are currently in, also does so.
169dee01b82SJohn Snow            kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES
170dee01b82SJohn Snow
171f1ad527fSJohn Snow        # ensurepip is slow: venv creation can be very fast for cases where
172f1ad527fSJohn Snow        # we allow the use of system_site_packages. Therefore, ensurepip is
173f1ad527fSJohn Snow        # replaced with our own script generation once the virtual environment
174f1ad527fSJohn Snow        # is setup.
175f1ad527fSJohn Snow        self.want_pip = kwargs.get("with_pip", False)
176f1ad527fSJohn Snow        if self.want_pip:
177f1ad527fSJohn Snow            if (
178f1ad527fSJohn Snow                kwargs.get("system_site_packages", False)
179f1ad527fSJohn Snow                and not need_ensurepip()
180f1ad527fSJohn Snow            ):
181f1ad527fSJohn Snow                kwargs["with_pip"] = False
182f1ad527fSJohn Snow            else:
1830a88ac96SPaolo Bonzini                check_ensurepip()
184a9dbde71SJohn Snow
185dd84028fSJohn Snow        super().__init__(*args, **kwargs)
186dd84028fSJohn Snow
187dd84028fSJohn Snow        # Make the context available post-creation:
188dd84028fSJohn Snow        self._context: Optional[SimpleNamespace] = None
189dd84028fSJohn Snow
190dee01b82SJohn Snow    def get_parent_libpath(self) -> Optional[str]:
191dee01b82SJohn Snow        """Return the libpath of the parent venv, if applicable."""
192dee01b82SJohn Snow        if self.use_parent_packages:
193dee01b82SJohn Snow            return sysconfig.get_path("purelib")
194dee01b82SJohn Snow        return None
195dee01b82SJohn Snow
196dee01b82SJohn Snow    @staticmethod
197dee01b82SJohn Snow    def compute_venv_libpath(context: SimpleNamespace) -> str:
198dee01b82SJohn Snow        """
199dee01b82SJohn Snow        Compatibility wrapper for context.lib_path for Python < 3.12
200dee01b82SJohn Snow        """
201dee01b82SJohn Snow        # Python 3.12+, not strictly necessary because it's documented
202dee01b82SJohn Snow        # to be the same as 3.10 code below:
203dee01b82SJohn Snow        if sys.version_info >= (3, 12):
204dee01b82SJohn Snow            return context.lib_path
205dee01b82SJohn Snow
206dee01b82SJohn Snow        # Python 3.10+
207dee01b82SJohn Snow        if "venv" in sysconfig.get_scheme_names():
208dee01b82SJohn Snow            lib_path = sysconfig.get_path(
209dee01b82SJohn Snow                "purelib", scheme="venv", vars={"base": context.env_dir}
210dee01b82SJohn Snow            )
211dee01b82SJohn Snow            assert lib_path is not None
212dee01b82SJohn Snow            return lib_path
213dee01b82SJohn Snow
214dee01b82SJohn Snow        # For Python <= 3.9 we need to hardcode this. Fortunately the
215dee01b82SJohn Snow        # code below was the same in Python 3.6-3.10, so there is only
216dee01b82SJohn Snow        # one case.
217dee01b82SJohn Snow        if sys.platform == "win32":
218dee01b82SJohn Snow            return os.path.join(context.env_dir, "Lib", "site-packages")
219dee01b82SJohn Snow        return os.path.join(
220dee01b82SJohn Snow            context.env_dir,
221dee01b82SJohn Snow            "lib",
222dee01b82SJohn Snow            "python%d.%d" % sys.version_info[:2],
223dee01b82SJohn Snow            "site-packages",
224dee01b82SJohn Snow        )
225dee01b82SJohn Snow
226dd84028fSJohn Snow    def ensure_directories(self, env_dir: DirType) -> SimpleNamespace:
227dd84028fSJohn Snow        logger.debug("ensure_directories(env_dir=%s)", env_dir)
228dd84028fSJohn Snow        self._context = super().ensure_directories(env_dir)
229dd84028fSJohn Snow        return self._context
230dd84028fSJohn Snow
231dee01b82SJohn Snow    def create(self, env_dir: DirType) -> None:
232dee01b82SJohn Snow        logger.debug("create(env_dir=%s)", env_dir)
233dee01b82SJohn Snow        super().create(env_dir)
234dee01b82SJohn Snow        assert self._context is not None
235dee01b82SJohn Snow        self.post_post_setup(self._context)
236dee01b82SJohn Snow
237dee01b82SJohn Snow    def post_post_setup(self, context: SimpleNamespace) -> None:
238dee01b82SJohn Snow        """
239dee01b82SJohn Snow        The final, final hook. Enter the venv and run commands inside of it.
240dee01b82SJohn Snow        """
241dee01b82SJohn Snow        if self.use_parent_packages:
242dee01b82SJohn Snow            # We're inside of a venv and we want to include the parent
243dee01b82SJohn Snow            # venv's packages.
244dee01b82SJohn Snow            parent_libpath = self.get_parent_libpath()
245dee01b82SJohn Snow            assert parent_libpath is not None
246dee01b82SJohn Snow            logger.debug("parent_libpath: %s", parent_libpath)
247dee01b82SJohn Snow
248dee01b82SJohn Snow            our_libpath = self.compute_venv_libpath(context)
249dee01b82SJohn Snow            logger.debug("our_libpath: %s", our_libpath)
250dee01b82SJohn Snow
251dee01b82SJohn Snow            pth_file = os.path.join(our_libpath, "nested.pth")
252dee01b82SJohn Snow            with open(pth_file, "w", encoding="UTF-8") as file:
253dee01b82SJohn Snow                file.write(parent_libpath + os.linesep)
254dee01b82SJohn Snow
255f1ad527fSJohn Snow        if self.want_pip:
256f1ad527fSJohn Snow            args = [
257f1ad527fSJohn Snow                context.env_exe,
258f1ad527fSJohn Snow                __file__,
259f1ad527fSJohn Snow                "post_init",
260f1ad527fSJohn Snow            ]
261f1ad527fSJohn Snow            subprocess.run(args, check=True)
262f1ad527fSJohn Snow
263dd84028fSJohn Snow    def get_value(self, field: str) -> str:
264dd84028fSJohn Snow        """
265dd84028fSJohn Snow        Get a string value from the context namespace after a call to build.
266dd84028fSJohn Snow
267dd84028fSJohn Snow        For valid field names, see:
268dd84028fSJohn Snow        https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories
269dd84028fSJohn Snow        """
270dd84028fSJohn Snow        ret = getattr(self._context, field)
271dd84028fSJohn Snow        assert isinstance(ret, str)
272dd84028fSJohn Snow        return ret
273dd84028fSJohn Snow
274dd84028fSJohn Snow
275f1ad527fSJohn Snowdef need_ensurepip() -> bool:
276f1ad527fSJohn Snow    """
277f1ad527fSJohn Snow    Tests for the presence of setuptools and pip.
278f1ad527fSJohn Snow
279f1ad527fSJohn Snow    :return: `True` if we do not detect both packages.
280f1ad527fSJohn Snow    """
281f1ad527fSJohn Snow    # Don't try to actually import them, it's fraught with danger:
282f1ad527fSJohn Snow    # https://github.com/pypa/setuptools/issues/2993
283f1ad527fSJohn Snow    if find_spec("setuptools") and find_spec("pip"):
284f1ad527fSJohn Snow        return False
285f1ad527fSJohn Snow    return True
286f1ad527fSJohn Snow
287f1ad527fSJohn Snow
2880a88ac96SPaolo Bonzinidef check_ensurepip() -> None:
289a9dbde71SJohn Snow    """
290a9dbde71SJohn Snow    Check that we have ensurepip.
291a9dbde71SJohn Snow
292a9dbde71SJohn Snow    Raise a fatal exception with a helpful hint if it isn't available.
293a9dbde71SJohn Snow    """
294a9dbde71SJohn Snow    if not find_spec("ensurepip"):
295a9dbde71SJohn Snow        msg = (
296a9dbde71SJohn Snow            "Python's ensurepip module is not found.\n"
297a9dbde71SJohn Snow            "It's normally part of the Python standard library, "
298a9dbde71SJohn Snow            "maybe your distribution packages it separately?\n"
299a9dbde71SJohn Snow            "Either install ensurepip, or alleviate the need for it in the "
300a9dbde71SJohn Snow            "first place by installing pip and setuptools for "
301a9dbde71SJohn Snow            f"'{sys.executable}'.\n"
3020a88ac96SPaolo Bonzini            "(Hint: Debian puts ensurepip in its python3-venv package.)"
303a9dbde71SJohn Snow        )
3040a88ac96SPaolo Bonzini        raise Ouch(msg)
305a9dbde71SJohn Snow
306a9dbde71SJohn Snow    # ensurepip uses pyexpat, which can also go missing on us:
307a9dbde71SJohn Snow    if not find_spec("pyexpat"):
308a9dbde71SJohn Snow        msg = (
309a9dbde71SJohn Snow            "Python's pyexpat module is not found.\n"
310a9dbde71SJohn Snow            "It's normally part of the Python standard library, "
311a9dbde71SJohn Snow            "maybe your distribution packages it separately?\n"
312a9dbde71SJohn Snow            "Either install pyexpat, or alleviate the need for it in the "
313a9dbde71SJohn Snow            "first place by installing pip and setuptools for "
3140a88ac96SPaolo Bonzini            f"'{sys.executable}'.\n\n"
3150a88ac96SPaolo Bonzini            "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)"
316a9dbde71SJohn Snow        )
3170a88ac96SPaolo Bonzini        raise Ouch(msg)
318a9dbde71SJohn Snow
319a9dbde71SJohn Snow
320dd84028fSJohn Snowdef make_venv(  # pylint: disable=too-many-arguments
321dd84028fSJohn Snow    env_dir: Union[str, Path],
322dd84028fSJohn Snow    system_site_packages: bool = False,
323dd84028fSJohn Snow    clear: bool = True,
324dd84028fSJohn Snow    symlinks: Optional[bool] = None,
325dd84028fSJohn Snow    with_pip: bool = True,
326dd84028fSJohn Snow) -> None:
327dd84028fSJohn Snow    """
328dd84028fSJohn Snow    Create a venv using `QemuEnvBuilder`.
329dd84028fSJohn Snow
330dd84028fSJohn Snow    This is analogous to the `venv.create` module-level convenience
331dd84028fSJohn Snow    function that is part of the Python stdblib, except it uses
332dd84028fSJohn Snow    `QemuEnvBuilder` instead.
333dd84028fSJohn Snow
334dd84028fSJohn Snow    :param env_dir: The directory to create/install to.
335dd84028fSJohn Snow    :param system_site_packages:
336dd84028fSJohn Snow        Allow inheriting packages from the system installation.
337dd84028fSJohn Snow    :param clear: When True, fully remove any prior venv and files.
338dd84028fSJohn Snow    :param symlinks:
339dd84028fSJohn Snow        Whether to use symlinks to the target interpreter or not. If
340dd84028fSJohn Snow        left unspecified, it will use symlinks except on Windows to
341dd84028fSJohn Snow        match behavior with the "venv" CLI tool.
342dd84028fSJohn Snow    :param with_pip:
343dd84028fSJohn Snow        Whether to install "pip" binaries or not.
344dd84028fSJohn Snow    """
345dd84028fSJohn Snow    logger.debug(
346dd84028fSJohn Snow        "%s: make_venv(env_dir=%s, system_site_packages=%s, "
347dd84028fSJohn Snow        "clear=%s, symlinks=%s, with_pip=%s)",
348dd84028fSJohn Snow        __file__,
349dd84028fSJohn Snow        str(env_dir),
350dd84028fSJohn Snow        system_site_packages,
351dd84028fSJohn Snow        clear,
352dd84028fSJohn Snow        symlinks,
353dd84028fSJohn Snow        with_pip,
354dd84028fSJohn Snow    )
355dd84028fSJohn Snow
356dd84028fSJohn Snow    if symlinks is None:
357dd84028fSJohn Snow        # Default behavior of standard venv CLI
358dd84028fSJohn Snow        symlinks = os.name != "nt"
359dd84028fSJohn Snow
360dd84028fSJohn Snow    builder = QemuEnvBuilder(
361dd84028fSJohn Snow        system_site_packages=system_site_packages,
362dd84028fSJohn Snow        clear=clear,
363dd84028fSJohn Snow        symlinks=symlinks,
364dd84028fSJohn Snow        with_pip=with_pip,
365dd84028fSJohn Snow    )
366dd84028fSJohn Snow
367dd84028fSJohn Snow    style = "non-isolated" if builder.system_site_packages else "isolated"
368dee01b82SJohn Snow    nested = ""
369dee01b82SJohn Snow    if builder.use_parent_packages:
370dee01b82SJohn Snow        nested = f"(with packages from '{builder.get_parent_libpath()}') "
371dd84028fSJohn Snow    print(
372dd84028fSJohn Snow        f"mkvenv: Creating {style} virtual environment"
373dee01b82SJohn Snow        f" {nested}at '{str(env_dir)}'",
374dd84028fSJohn Snow        file=sys.stderr,
375dd84028fSJohn Snow    )
376dd84028fSJohn Snow
377dd84028fSJohn Snow    try:
378dd84028fSJohn Snow        logger.debug("Invoking builder.create()")
379dd84028fSJohn Snow        try:
380dd84028fSJohn Snow            builder.create(str(env_dir))
381dd84028fSJohn Snow        except SystemExit as exc:
382*05fd7214SJohn Snow            # pylint 3.3 bug:
383*05fd7214SJohn Snow            # pylint: disable=raising-non-exception, raise-missing-from
384*05fd7214SJohn Snow
385dd84028fSJohn Snow            # Some versions of the venv module raise SystemExit; *nasty*!
386dd84028fSJohn Snow            # We want the exception that prompted it. It might be a subprocess
387dd84028fSJohn Snow            # error that has output we *really* want to see.
388dd84028fSJohn Snow            logger.debug("Intercepted SystemExit from EnvBuilder.create()")
389dd84028fSJohn Snow            raise exc.__cause__ or exc.__context__ or exc
390dd84028fSJohn Snow        logger.debug("builder.create() finished")
391dd84028fSJohn Snow    except subprocess.CalledProcessError as exc:
392dd84028fSJohn Snow        logger.error("mkvenv subprocess failed:")
393dd84028fSJohn Snow        logger.error("cmd: %s", exc.cmd)
394dd84028fSJohn Snow        logger.error("returncode: %d", exc.returncode)
395dd84028fSJohn Snow
396dd84028fSJohn Snow        def _stringify(data: Union[str, bytes]) -> str:
397dd84028fSJohn Snow            if isinstance(data, bytes):
398dd84028fSJohn Snow                return data.decode()
399dd84028fSJohn Snow            return data
400dd84028fSJohn Snow
401dd84028fSJohn Snow        lines = []
402dd84028fSJohn Snow        if exc.stdout:
403dd84028fSJohn Snow            lines.append("========== stdout ==========")
404dd84028fSJohn Snow            lines.append(_stringify(exc.stdout))
405dd84028fSJohn Snow            lines.append("============================")
406dd84028fSJohn Snow        if exc.stderr:
407dd84028fSJohn Snow            lines.append("========== stderr ==========")
408dd84028fSJohn Snow            lines.append(_stringify(exc.stderr))
409dd84028fSJohn Snow            lines.append("============================")
410dd84028fSJohn Snow        if lines:
411dd84028fSJohn Snow            logger.error(os.linesep.join(lines))
412dd84028fSJohn Snow
413dd84028fSJohn Snow        raise Ouch("VENV creation subprocess failed.") from exc
414dd84028fSJohn Snow
415dd84028fSJohn Snow    # print the python executable to stdout for configure.
416dd84028fSJohn Snow    print(builder.get_value("env_exe"))
417dd84028fSJohn Snow
418dd84028fSJohn Snow
4193e4b6b0aSPaolo Bonzinidef _get_entry_points(packages: Sequence[str]) -> Iterator[str]:
42092834894SJohn Snow
42192834894SJohn Snow    def _generator() -> Iterator[str]:
42292834894SJohn Snow        for package in packages:
42392834894SJohn Snow            try:
424ca056f44SPaolo Bonzini                entry_points: Iterator[EntryPoint] = \
425ca056f44SPaolo Bonzini                    iter(distribution(package).entry_points)
42692834894SJohn Snow            except PackageNotFoundError:
42792834894SJohn Snow                continue
42892834894SJohn Snow
42992834894SJohn Snow            # The EntryPoints type is only available in 3.10+,
43092834894SJohn Snow            # treat this as a vanilla list and filter it ourselves.
43192834894SJohn Snow            entry_points = filter(
43292834894SJohn Snow                lambda ep: ep.group == "console_scripts", entry_points
43392834894SJohn Snow            )
43492834894SJohn Snow
43592834894SJohn Snow            for entry_point in entry_points:
43692834894SJohn Snow                yield f"{entry_point.name} = {entry_point.value}"
43792834894SJohn Snow
43892834894SJohn Snow    return _generator()
43992834894SJohn Snow
44092834894SJohn Snow
44192834894SJohn Snowdef generate_console_scripts(
44292834894SJohn Snow    packages: Sequence[str],
44392834894SJohn Snow    python_path: Optional[str] = None,
44492834894SJohn Snow    bin_path: Optional[str] = None,
44592834894SJohn Snow) -> None:
44692834894SJohn Snow    """
44792834894SJohn Snow    Generate script shims for console_script entry points in @packages.
44892834894SJohn Snow    """
44992834894SJohn Snow    if python_path is None:
45092834894SJohn Snow        python_path = sys.executable
45192834894SJohn Snow    if bin_path is None:
45292834894SJohn Snow        bin_path = sysconfig.get_path("scripts")
45392834894SJohn Snow        assert bin_path is not None
45492834894SJohn Snow
45592834894SJohn Snow    logger.debug(
45692834894SJohn Snow        "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)",
45792834894SJohn Snow        packages,
45892834894SJohn Snow        python_path,
45992834894SJohn Snow        bin_path,
46092834894SJohn Snow    )
46192834894SJohn Snow
46292834894SJohn Snow    if not packages:
46392834894SJohn Snow        return
46492834894SJohn Snow
46592834894SJohn Snow    maker = distlib.scripts.ScriptMaker(None, bin_path)
46692834894SJohn Snow    maker.variants = {""}
46792834894SJohn Snow    maker.clobber = False
46892834894SJohn Snow
4693e4b6b0aSPaolo Bonzini    for entry_point in _get_entry_points(packages):
47092834894SJohn Snow        for filename in maker.make(entry_point):
47192834894SJohn Snow            logger.debug("wrote console_script '%s'", filename)
47292834894SJohn Snow
47392834894SJohn Snow
4744695a22eSJohn Snowdef pkgname_from_depspec(dep_spec: str) -> str:
4754695a22eSJohn Snow    """
4764695a22eSJohn Snow    Parse package name out of a PEP-508 depspec.
4774695a22eSJohn Snow
4784695a22eSJohn Snow    See https://peps.python.org/pep-0508/#names
4794695a22eSJohn Snow    """
4804695a22eSJohn Snow    match = re.match(
4814695a22eSJohn Snow        r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE
4824695a22eSJohn Snow    )
4834695a22eSJohn Snow    if not match:
4844695a22eSJohn Snow        raise ValueError(
4854695a22eSJohn Snow            f"dep_spec '{dep_spec}'"
4864695a22eSJohn Snow            " does not appear to contain a valid package name"
4874695a22eSJohn Snow        )
4884695a22eSJohn Snow    return match.group(0)
4894695a22eSJohn Snow
4904695a22eSJohn Snow
49147a90a51SPaolo Bonzinidef _path_is_prefix(prefix: Optional[str], path: str) -> bool:
49247a90a51SPaolo Bonzini    try:
49347a90a51SPaolo Bonzini        return (
49447a90a51SPaolo Bonzini            prefix is not None and os.path.commonpath([prefix, path]) == prefix
49547a90a51SPaolo Bonzini        )
49647a90a51SPaolo Bonzini    except ValueError:
49747a90a51SPaolo Bonzini        return False
49847a90a51SPaolo Bonzini
49947a90a51SPaolo Bonzini
5003e4b6b0aSPaolo Bonzinidef _is_system_package(dist: Distribution) -> bool:
5013e4b6b0aSPaolo Bonzini    path = str(dist.locate_file("."))
5023e4b6b0aSPaolo Bonzini    return not (
50347a90a51SPaolo Bonzini        _path_is_prefix(sysconfig.get_path("purelib"), path)
50447a90a51SPaolo Bonzini        or _path_is_prefix(sysconfig.get_path("platlib"), path)
50547a90a51SPaolo Bonzini    )
50647a90a51SPaolo Bonzini
50747a90a51SPaolo Bonzini
5084695a22eSJohn Snowdef diagnose(
5094695a22eSJohn Snow    dep_spec: str,
5104695a22eSJohn Snow    online: bool,
5114695a22eSJohn Snow    wheels_dir: Optional[Union[str, Path]],
5124695a22eSJohn Snow    prog: Optional[str],
5134695a22eSJohn Snow) -> Tuple[str, bool]:
5144695a22eSJohn Snow    """
5154695a22eSJohn Snow    Offer a summary to the user as to why a package failed to be installed.
5164695a22eSJohn Snow
5174695a22eSJohn Snow    :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5'
5184695a22eSJohn Snow    :param online: Did we allow PyPI access?
5194695a22eSJohn Snow    :param prog:
5204695a22eSJohn Snow        Optionally, a shell program name that can be used as a
5214695a22eSJohn Snow        bellwether to detect if this program is installed elsewhere on
5224695a22eSJohn Snow        the system. This is used to offer advice when a program is
5234695a22eSJohn Snow        detected for a different python version.
5244695a22eSJohn Snow    :param wheels_dir:
5254695a22eSJohn Snow        Optionally, a directory that was searched for vendored packages.
5264695a22eSJohn Snow    """
5274695a22eSJohn Snow    # pylint: disable=too-many-branches
5284695a22eSJohn Snow
5294695a22eSJohn Snow    # Some errors are not particularly serious
5304695a22eSJohn Snow    bad = False
5314695a22eSJohn Snow
5324695a22eSJohn Snow    pkg_name = pkgname_from_depspec(dep_spec)
5333e4b6b0aSPaolo Bonzini    pkg_version: Optional[str] = None
5343e4b6b0aSPaolo Bonzini    try:
5353e4b6b0aSPaolo Bonzini        pkg_version = version(pkg_name)
5363e4b6b0aSPaolo Bonzini    except PackageNotFoundError:
5373e4b6b0aSPaolo Bonzini        pass
5384695a22eSJohn Snow
5394695a22eSJohn Snow    lines = []
5404695a22eSJohn Snow
5414695a22eSJohn Snow    if pkg_version:
5424695a22eSJohn Snow        lines.append(
5434695a22eSJohn Snow            f"Python package '{pkg_name}' version '{pkg_version}' was found,"
5444695a22eSJohn Snow            " but isn't suitable."
5454695a22eSJohn Snow        )
5464695a22eSJohn Snow    else:
5474695a22eSJohn Snow        lines.append(
548c673f3d0SPaolo Bonzini            f"Python package '{pkg_name}' was not found nor installed."
5494695a22eSJohn Snow        )
5504695a22eSJohn Snow
5514695a22eSJohn Snow    if wheels_dir:
5524695a22eSJohn Snow        lines.append(
5534695a22eSJohn Snow            "No suitable version found in, or failed to install from"
5544695a22eSJohn Snow            f" '{wheels_dir}'."
5554695a22eSJohn Snow        )
5564695a22eSJohn Snow        bad = True
5574695a22eSJohn Snow
5584695a22eSJohn Snow    if online:
5594695a22eSJohn Snow        lines.append("A suitable version could not be obtained from PyPI.")
5604695a22eSJohn Snow        bad = True
5614695a22eSJohn Snow    else:
5624695a22eSJohn Snow        lines.append(
5634695a22eSJohn Snow            "mkvenv was configured to operate offline and did not check PyPI."
5644695a22eSJohn Snow        )
5654695a22eSJohn Snow
5664695a22eSJohn Snow    if prog and not pkg_version:
5674695a22eSJohn Snow        which = shutil.which(prog)
5684695a22eSJohn Snow        if which:
5694695a22eSJohn Snow            if sys.base_prefix in site.PREFIXES:
5704695a22eSJohn Snow                pypath = Path(sys.executable).resolve()
5714695a22eSJohn Snow                lines.append(
5724695a22eSJohn Snow                    f"'{prog}' was detected on your system at '{which}', "
5734695a22eSJohn Snow                    f"but the Python package '{pkg_name}' was not found by "
5744695a22eSJohn Snow                    f"this Python interpreter ('{pypath}'). "
5754695a22eSJohn Snow                    f"Typically this means that '{prog}' has been installed "
5764695a22eSJohn Snow                    "against a different Python interpreter on your system."
5774695a22eSJohn Snow                )
5784695a22eSJohn Snow            else:
5794695a22eSJohn Snow                lines.append(
5804695a22eSJohn Snow                    f"'{prog}' was detected on your system at '{which}', "
5814695a22eSJohn Snow                    "but the build is using an isolated virtual environment."
5824695a22eSJohn Snow                )
5834695a22eSJohn Snow            bad = True
5844695a22eSJohn Snow
5854695a22eSJohn Snow    lines = [f" • {line}" for line in lines]
5864695a22eSJohn Snow    if bad:
5874695a22eSJohn Snow        lines.insert(0, f"Could not provide build dependency '{dep_spec}':")
5884695a22eSJohn Snow    else:
5894695a22eSJohn Snow        lines.insert(0, f"'{dep_spec}' not found:")
5904695a22eSJohn Snow    return os.linesep.join(lines), bad
5914695a22eSJohn Snow
5924695a22eSJohn Snow
593c5538eedSJohn Snowdef pip_install(
594c5538eedSJohn Snow    args: Sequence[str],
595c5538eedSJohn Snow    online: bool = False,
596c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
597c5538eedSJohn Snow) -> None:
598c5538eedSJohn Snow    """
599c5538eedSJohn Snow    Use pip to install a package or package(s) as specified in @args.
600c5538eedSJohn Snow    """
601c5538eedSJohn Snow    loud = bool(
602c5538eedSJohn Snow        os.environ.get("DEBUG")
603c5538eedSJohn Snow        or os.environ.get("GITLAB_CI")
604c5538eedSJohn Snow        or os.environ.get("V")
605c5538eedSJohn Snow    )
606c5538eedSJohn Snow
607c5538eedSJohn Snow    full_args = [
608c5538eedSJohn Snow        sys.executable,
609c5538eedSJohn Snow        "-m",
610c5538eedSJohn Snow        "pip",
611c5538eedSJohn Snow        "install",
612c5538eedSJohn Snow        "--disable-pip-version-check",
613c5538eedSJohn Snow        "-v" if loud else "-q",
614c5538eedSJohn Snow    ]
615c5538eedSJohn Snow    if not online:
616c5538eedSJohn Snow        full_args += ["--no-index"]
617c5538eedSJohn Snow    if wheels_dir:
618c5538eedSJohn Snow        full_args += ["--find-links", f"file://{str(wheels_dir)}"]
619c5538eedSJohn Snow    full_args += list(args)
620c5538eedSJohn Snow    subprocess.run(
621c5538eedSJohn Snow        full_args,
622c5538eedSJohn Snow        check=True,
623c5538eedSJohn Snow    )
624c5538eedSJohn Snow
625c5538eedSJohn Snow
6260f1ec070SPaolo Bonzinidef _make_version_constraint(info: Dict[str, str], install: bool) -> str:
6270f1ec070SPaolo Bonzini    """
6280f1ec070SPaolo Bonzini    Construct the version constraint part of a PEP 508 dependency
6290f1ec070SPaolo Bonzini    specification (for example '>=0.61.5') from the accepted and
6300f1ec070SPaolo Bonzini    installed keys of the provided dictionary.
6310f1ec070SPaolo Bonzini
6320f1ec070SPaolo Bonzini    :param info: A dictionary corresponding to a TOML key-value list.
6330f1ec070SPaolo Bonzini    :param install: True generates install constraints, False generates
6340f1ec070SPaolo Bonzini        presence constraints
6350f1ec070SPaolo Bonzini    """
6360f1ec070SPaolo Bonzini    if install and "installed" in info:
6370f1ec070SPaolo Bonzini        return "==" + info["installed"]
6380f1ec070SPaolo Bonzini
6390f1ec070SPaolo Bonzini    dep_spec = info.get("accepted", "")
6400f1ec070SPaolo Bonzini    dep_spec = dep_spec.strip()
6410f1ec070SPaolo Bonzini    # Double check that they didn't just use a version number
6420f1ec070SPaolo Bonzini    if dep_spec and dep_spec[0] not in "!~><=(":
6430f1ec070SPaolo Bonzini        raise Ouch(
6440f1ec070SPaolo Bonzini            "invalid dependency specifier " + dep_spec + " in dependency file"
6450f1ec070SPaolo Bonzini        )
6460f1ec070SPaolo Bonzini
6470f1ec070SPaolo Bonzini    return dep_spec
6480f1ec070SPaolo Bonzini
6490f1ec070SPaolo Bonzini
6504695a22eSJohn Snowdef _do_ensure(
6510f1ec070SPaolo Bonzini    group: Dict[str, Dict[str, str]],
652c5538eedSJohn Snow    online: bool = False,
653c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
654d37c21b5SPaolo Bonzini) -> Optional[Tuple[str, bool]]:
655c5538eedSJohn Snow    """
6560f1ec070SPaolo Bonzini    Use pip to ensure we have the packages specified in @group.
657c5538eedSJohn Snow
6580f1ec070SPaolo Bonzini    If the packages are already installed, do nothing. If online and
659c5538eedSJohn Snow    wheels_dir are both provided, prefer packages found in wheels_dir
660c5538eedSJohn Snow    first before connecting to PyPI.
661c5538eedSJohn Snow
6620f1ec070SPaolo Bonzini    :param group: A dictionary of dictionaries, corresponding to a
6630f1ec070SPaolo Bonzini        section in a pythondeps.toml file.
664c5538eedSJohn Snow    :param online: If True, fall back to PyPI.
665c5538eedSJohn Snow    :param wheels_dir: If specified, search this path for packages.
666c5538eedSJohn Snow    """
667c5538eedSJohn Snow    absent = []
66892834894SJohn Snow    present = []
66967b9a83dSPaolo Bonzini    canary = None
6700f1ec070SPaolo Bonzini    for name, info in group.items():
6710f1ec070SPaolo Bonzini        constraint = _make_version_constraint(info, False)
6720f1ec070SPaolo Bonzini        matcher = distlib.version.LegacyMatcher(name + constraint)
67371ed611cSPaolo Bonzini        print(f"mkvenv: checking for {matcher}", file=sys.stderr)
6743e4b6b0aSPaolo Bonzini
6753e4b6b0aSPaolo Bonzini        dist: Optional[Distribution] = None
6763e4b6b0aSPaolo Bonzini        try:
6773e4b6b0aSPaolo Bonzini            dist = distribution(matcher.name)
6783e4b6b0aSPaolo Bonzini        except PackageNotFoundError:
6793e4b6b0aSPaolo Bonzini            pass
6803e4b6b0aSPaolo Bonzini
68147a90a51SPaolo Bonzini        if (
6823e4b6b0aSPaolo Bonzini            dist is None
68347a90a51SPaolo Bonzini            # Always pass installed package to pip, so that they can be
68447a90a51SPaolo Bonzini            # updated if the requested version changes
6853e4b6b0aSPaolo Bonzini            or not _is_system_package(dist)
6863e4b6b0aSPaolo Bonzini            or not matcher.match(distlib.version.LegacyVersion(dist.version))
687c673f3d0SPaolo Bonzini        ):
6880f1ec070SPaolo Bonzini            absent.append(name + _make_version_constraint(info, True))
6890f1ec070SPaolo Bonzini            if len(absent) == 1:
6900f1ec070SPaolo Bonzini                canary = info.get("canary", None)
691c5538eedSJohn Snow        else:
6923e4b6b0aSPaolo Bonzini            logger.info("found %s %s", name, dist.version)
6930f1ec070SPaolo Bonzini            present.append(name)
69492834894SJohn Snow
69592834894SJohn Snow    if present:
69692834894SJohn Snow        generate_console_scripts(present)
697c5538eedSJohn Snow
698c5538eedSJohn Snow    if absent:
699d37c21b5SPaolo Bonzini        if online or wheels_dir:
700c5538eedSJohn Snow            # Some packages are missing or aren't a suitable version,
701c5538eedSJohn Snow            # install a suitable (possibly vendored) package.
702c5538eedSJohn Snow            print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr)
703d37c21b5SPaolo Bonzini            try:
704c5538eedSJohn Snow                pip_install(args=absent, online=online, wheels_dir=wheels_dir)
705d37c21b5SPaolo Bonzini                return None
706d37c21b5SPaolo Bonzini            except subprocess.CalledProcessError:
707d37c21b5SPaolo Bonzini                pass
708d37c21b5SPaolo Bonzini
709d37c21b5SPaolo Bonzini        return diagnose(
710d37c21b5SPaolo Bonzini            absent[0],
711d37c21b5SPaolo Bonzini            online,
712d37c21b5SPaolo Bonzini            wheels_dir,
71367b9a83dSPaolo Bonzini            canary,
714d37c21b5SPaolo Bonzini        )
715d37c21b5SPaolo Bonzini
716d37c21b5SPaolo Bonzini    return None
717c5538eedSJohn Snow
718c5538eedSJohn Snow
71971ed611cSPaolo Bonzinidef _parse_groups(file: str) -> Dict[str, Dict[str, Any]]:
72071ed611cSPaolo Bonzini    if not HAVE_TOMLLIB:
72171ed611cSPaolo Bonzini        if sys.version_info < (3, 11):
72271ed611cSPaolo Bonzini            raise Ouch("found no usable tomli, please install it")
72371ed611cSPaolo Bonzini
72471ed611cSPaolo Bonzini        raise Ouch(
72571ed611cSPaolo Bonzini            "Python >=3.11 does not have tomllib... what have you done!?"
72671ed611cSPaolo Bonzini        )
72771ed611cSPaolo Bonzini
72871ed611cSPaolo Bonzini    # Use loads() to support both tomli v1.2.x (Ubuntu 22.04,
72971ed611cSPaolo Bonzini    # Debian bullseye-backports) and v2.0.x
73071ed611cSPaolo Bonzini    with open(file, "r", encoding="ascii") as depfile:
73171ed611cSPaolo Bonzini        contents = depfile.read()
73271ed611cSPaolo Bonzini        return tomllib.loads(contents)  # type: ignore
73371ed611cSPaolo Bonzini
73471ed611cSPaolo Bonzini
73571ed611cSPaolo Bonzinidef ensure_group(
73671ed611cSPaolo Bonzini    file: str,
73771ed611cSPaolo Bonzini    groups: Sequence[str],
73871ed611cSPaolo Bonzini    online: bool = False,
73971ed611cSPaolo Bonzini    wheels_dir: Optional[Union[str, Path]] = None,
74071ed611cSPaolo Bonzini) -> None:
74171ed611cSPaolo Bonzini    """
74271ed611cSPaolo Bonzini    Use pip to ensure we have the package specified by @dep_specs.
74371ed611cSPaolo Bonzini
74471ed611cSPaolo Bonzini    If the package is already installed, do nothing. If online and
74571ed611cSPaolo Bonzini    wheels_dir are both provided, prefer packages found in wheels_dir
74671ed611cSPaolo Bonzini    first before connecting to PyPI.
74771ed611cSPaolo Bonzini
74871ed611cSPaolo Bonzini    :param dep_specs:
74971ed611cSPaolo Bonzini        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
75071ed611cSPaolo Bonzini    :param online: If True, fall back to PyPI.
75171ed611cSPaolo Bonzini    :param wheels_dir: If specified, search this path for packages.
75271ed611cSPaolo Bonzini    """
75371ed611cSPaolo Bonzini
75471ed611cSPaolo Bonzini    if not HAVE_DISTLIB:
75571ed611cSPaolo Bonzini        raise Ouch("found no usable distlib, please install it")
75671ed611cSPaolo Bonzini
75771ed611cSPaolo Bonzini    parsed_deps = _parse_groups(file)
75871ed611cSPaolo Bonzini
75971ed611cSPaolo Bonzini    to_install: Dict[str, Dict[str, str]] = {}
76071ed611cSPaolo Bonzini    for group in groups:
76171ed611cSPaolo Bonzini        try:
76271ed611cSPaolo Bonzini            to_install.update(parsed_deps[group])
76371ed611cSPaolo Bonzini        except KeyError as exc:
76471ed611cSPaolo Bonzini            raise Ouch(f"group {group} not defined") from exc
76571ed611cSPaolo Bonzini
76671ed611cSPaolo Bonzini    result = _do_ensure(to_install, online, wheels_dir)
76771ed611cSPaolo Bonzini    if result:
76871ed611cSPaolo Bonzini        # Well, that's not good.
76971ed611cSPaolo Bonzini        if result[1]:
77071ed611cSPaolo Bonzini            raise Ouch(result[0])
77171ed611cSPaolo Bonzini        raise SystemExit(f"\n{result[0]}\n\n")
77271ed611cSPaolo Bonzini
77371ed611cSPaolo Bonzini
774f1ad527fSJohn Snowdef post_venv_setup() -> None:
775f1ad527fSJohn Snow    """
776f1ad527fSJohn Snow    This is intended to be run *inside the venv* after it is created.
777f1ad527fSJohn Snow    """
778f1ad527fSJohn Snow    logger.debug("post_venv_setup()")
7790a88ac96SPaolo Bonzini    # Generate a 'pip' script so the venv is usable in a normal
780f1ad527fSJohn Snow    # way from the CLI. This only happens when we inherited pip from a
781f1ad527fSJohn Snow    # parent/system-site and haven't run ensurepip in some way.
782f1ad527fSJohn Snow    generate_console_scripts(["pip"])
783f1ad527fSJohn Snow
784f1ad527fSJohn Snow
785dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None:
786dd84028fSJohn Snow    subparser = subparsers.add_parser("create", help="create a venv")
787dd84028fSJohn Snow    subparser.add_argument(
788dd84028fSJohn Snow        "target",
789dd84028fSJohn Snow        type=str,
790dd84028fSJohn Snow        action="store",
791dd84028fSJohn Snow        help="Target directory to install virtual environment into.",
792dd84028fSJohn Snow    )
793dd84028fSJohn Snow
794dd84028fSJohn Snow
795f1ad527fSJohn Snowdef _add_post_init_subcommand(subparsers: Any) -> None:
796f1ad527fSJohn Snow    subparsers.add_parser("post_init", help="post-venv initialization")
797f1ad527fSJohn Snow
798f1ad527fSJohn Snow
79971ed611cSPaolo Bonzinidef _add_ensuregroup_subcommand(subparsers: Any) -> None:
80071ed611cSPaolo Bonzini    subparser = subparsers.add_parser(
80171ed611cSPaolo Bonzini        "ensuregroup",
80271ed611cSPaolo Bonzini        help="Ensure that the specified package group is installed.",
80371ed611cSPaolo Bonzini    )
80471ed611cSPaolo Bonzini    subparser.add_argument(
80571ed611cSPaolo Bonzini        "--online",
80671ed611cSPaolo Bonzini        action="store_true",
80771ed611cSPaolo Bonzini        help="Install packages from PyPI, if necessary.",
80871ed611cSPaolo Bonzini    )
80971ed611cSPaolo Bonzini    subparser.add_argument(
81071ed611cSPaolo Bonzini        "--dir",
81171ed611cSPaolo Bonzini        type=str,
81271ed611cSPaolo Bonzini        action="store",
81371ed611cSPaolo Bonzini        help="Path to vendored packages where we may install from.",
81471ed611cSPaolo Bonzini    )
81571ed611cSPaolo Bonzini    subparser.add_argument(
81671ed611cSPaolo Bonzini        "file",
81771ed611cSPaolo Bonzini        type=str,
81871ed611cSPaolo Bonzini        action="store",
81971ed611cSPaolo Bonzini        help=("Path to a TOML file describing package groups"),
82071ed611cSPaolo Bonzini    )
82171ed611cSPaolo Bonzini    subparser.add_argument(
82271ed611cSPaolo Bonzini        "group",
82371ed611cSPaolo Bonzini        type=str,
82471ed611cSPaolo Bonzini        action="store",
82571ed611cSPaolo Bonzini        help="One or more package group names",
82671ed611cSPaolo Bonzini        nargs="+",
82771ed611cSPaolo Bonzini    )
82871ed611cSPaolo Bonzini
82971ed611cSPaolo Bonzini
830dd84028fSJohn Snowdef main() -> int:
831dd84028fSJohn Snow    """CLI interface to make_qemu_venv. See module docstring."""
832dd84028fSJohn Snow    if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
833dd84028fSJohn Snow        # You're welcome.
834dd84028fSJohn Snow        logging.basicConfig(level=logging.DEBUG)
835c5538eedSJohn Snow    else:
836c5538eedSJohn Snow        if os.environ.get("V"):
837dd84028fSJohn Snow            logging.basicConfig(level=logging.INFO)
838dd84028fSJohn Snow
839dd84028fSJohn Snow    parser = argparse.ArgumentParser(
840dd84028fSJohn Snow        prog="mkvenv",
841dd84028fSJohn Snow        description="QEMU pyvenv bootstrapping utility",
842dd84028fSJohn Snow    )
843dd84028fSJohn Snow    subparsers = parser.add_subparsers(
844dd84028fSJohn Snow        title="Commands",
845dd84028fSJohn Snow        dest="command",
84602312f1aSPaolo Bonzini        required=True,
847dd84028fSJohn Snow        metavar="command",
848dd84028fSJohn Snow        help="Description",
849dd84028fSJohn Snow    )
850dd84028fSJohn Snow
851dd84028fSJohn Snow    _add_create_subcommand(subparsers)
852f1ad527fSJohn Snow    _add_post_init_subcommand(subparsers)
85371ed611cSPaolo Bonzini    _add_ensuregroup_subcommand(subparsers)
854dd84028fSJohn Snow
855dd84028fSJohn Snow    args = parser.parse_args()
856dd84028fSJohn Snow    try:
857dd84028fSJohn Snow        if args.command == "create":
858dd84028fSJohn Snow            make_venv(
859dd84028fSJohn Snow                args.target,
860dd84028fSJohn Snow                system_site_packages=True,
861dd84028fSJohn Snow                clear=True,
862dd84028fSJohn Snow            )
863f1ad527fSJohn Snow        if args.command == "post_init":
864f1ad527fSJohn Snow            post_venv_setup()
86571ed611cSPaolo Bonzini        if args.command == "ensuregroup":
86671ed611cSPaolo Bonzini            ensure_group(
86771ed611cSPaolo Bonzini                file=args.file,
86871ed611cSPaolo Bonzini                groups=args.group,
86971ed611cSPaolo Bonzini                online=args.online,
87071ed611cSPaolo Bonzini                wheels_dir=args.dir,
87171ed611cSPaolo Bonzini            )
872dd84028fSJohn Snow        logger.debug("mkvenv.py %s: exiting", args.command)
873dd84028fSJohn Snow    except Ouch as exc:
874dd84028fSJohn Snow        print("\n*** Ouch! ***\n", file=sys.stderr)
875dd84028fSJohn Snow        print(str(exc), "\n\n", file=sys.stderr)
876dd84028fSJohn Snow        return 1
877dd84028fSJohn Snow    except SystemExit:
878dd84028fSJohn Snow        raise
879dd84028fSJohn Snow    except:  # pylint: disable=bare-except
880dd84028fSJohn Snow        logger.exception("mkvenv did not complete successfully:")
881dd84028fSJohn Snow        return 2
882dd84028fSJohn Snow    return 0
883dd84028fSJohn Snow
884dd84028fSJohn Snow
885dd84028fSJohn Snowif __name__ == "__main__":
886dd84028fSJohn Snow    sys.exit(main())
887