xref: /openbmc/qemu/python/scripts/mkvenv.py (revision fc00123f)
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:
183*0a88ac96SPaolo 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
288*0a88ac96SPaolo 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"
302*0a88ac96SPaolo Bonzini            "(Hint: Debian puts ensurepip in its python3-venv package.)"
303a9dbde71SJohn Snow        )
304*0a88ac96SPaolo 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 "
314*0a88ac96SPaolo Bonzini            f"'{sys.executable}'.\n\n"
315*0a88ac96SPaolo Bonzini            "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)"
316a9dbde71SJohn Snow        )
317*0a88ac96SPaolo 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:
382dd84028fSJohn Snow            # Some versions of the venv module raise SystemExit; *nasty*!
383dd84028fSJohn Snow            # We want the exception that prompted it. It might be a subprocess
384dd84028fSJohn Snow            # error that has output we *really* want to see.
385dd84028fSJohn Snow            logger.debug("Intercepted SystemExit from EnvBuilder.create()")
386dd84028fSJohn Snow            raise exc.__cause__ or exc.__context__ or exc
387dd84028fSJohn Snow        logger.debug("builder.create() finished")
388dd84028fSJohn Snow    except subprocess.CalledProcessError as exc:
389dd84028fSJohn Snow        logger.error("mkvenv subprocess failed:")
390dd84028fSJohn Snow        logger.error("cmd: %s", exc.cmd)
391dd84028fSJohn Snow        logger.error("returncode: %d", exc.returncode)
392dd84028fSJohn Snow
393dd84028fSJohn Snow        def _stringify(data: Union[str, bytes]) -> str:
394dd84028fSJohn Snow            if isinstance(data, bytes):
395dd84028fSJohn Snow                return data.decode()
396dd84028fSJohn Snow            return data
397dd84028fSJohn Snow
398dd84028fSJohn Snow        lines = []
399dd84028fSJohn Snow        if exc.stdout:
400dd84028fSJohn Snow            lines.append("========== stdout ==========")
401dd84028fSJohn Snow            lines.append(_stringify(exc.stdout))
402dd84028fSJohn Snow            lines.append("============================")
403dd84028fSJohn Snow        if exc.stderr:
404dd84028fSJohn Snow            lines.append("========== stderr ==========")
405dd84028fSJohn Snow            lines.append(_stringify(exc.stderr))
406dd84028fSJohn Snow            lines.append("============================")
407dd84028fSJohn Snow        if lines:
408dd84028fSJohn Snow            logger.error(os.linesep.join(lines))
409dd84028fSJohn Snow
410dd84028fSJohn Snow        raise Ouch("VENV creation subprocess failed.") from exc
411dd84028fSJohn Snow
412dd84028fSJohn Snow    # print the python executable to stdout for configure.
413dd84028fSJohn Snow    print(builder.get_value("env_exe"))
414dd84028fSJohn Snow
415dd84028fSJohn Snow
4163e4b6b0aSPaolo Bonzinidef _get_entry_points(packages: Sequence[str]) -> Iterator[str]:
41792834894SJohn Snow
41892834894SJohn Snow    def _generator() -> Iterator[str]:
41992834894SJohn Snow        for package in packages:
42092834894SJohn Snow            try:
421ca056f44SPaolo Bonzini                entry_points: Iterator[EntryPoint] = \
422ca056f44SPaolo Bonzini                    iter(distribution(package).entry_points)
42392834894SJohn Snow            except PackageNotFoundError:
42492834894SJohn Snow                continue
42592834894SJohn Snow
42692834894SJohn Snow            # The EntryPoints type is only available in 3.10+,
42792834894SJohn Snow            # treat this as a vanilla list and filter it ourselves.
42892834894SJohn Snow            entry_points = filter(
42992834894SJohn Snow                lambda ep: ep.group == "console_scripts", entry_points
43092834894SJohn Snow            )
43192834894SJohn Snow
43292834894SJohn Snow            for entry_point in entry_points:
43392834894SJohn Snow                yield f"{entry_point.name} = {entry_point.value}"
43492834894SJohn Snow
43592834894SJohn Snow    return _generator()
43692834894SJohn Snow
43792834894SJohn Snow
43892834894SJohn Snowdef generate_console_scripts(
43992834894SJohn Snow    packages: Sequence[str],
44092834894SJohn Snow    python_path: Optional[str] = None,
44192834894SJohn Snow    bin_path: Optional[str] = None,
44292834894SJohn Snow) -> None:
44392834894SJohn Snow    """
44492834894SJohn Snow    Generate script shims for console_script entry points in @packages.
44592834894SJohn Snow    """
44692834894SJohn Snow    if python_path is None:
44792834894SJohn Snow        python_path = sys.executable
44892834894SJohn Snow    if bin_path is None:
44992834894SJohn Snow        bin_path = sysconfig.get_path("scripts")
45092834894SJohn Snow        assert bin_path is not None
45192834894SJohn Snow
45292834894SJohn Snow    logger.debug(
45392834894SJohn Snow        "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)",
45492834894SJohn Snow        packages,
45592834894SJohn Snow        python_path,
45692834894SJohn Snow        bin_path,
45792834894SJohn Snow    )
45892834894SJohn Snow
45992834894SJohn Snow    if not packages:
46092834894SJohn Snow        return
46192834894SJohn Snow
46292834894SJohn Snow    maker = distlib.scripts.ScriptMaker(None, bin_path)
46392834894SJohn Snow    maker.variants = {""}
46492834894SJohn Snow    maker.clobber = False
46592834894SJohn Snow
4663e4b6b0aSPaolo Bonzini    for entry_point in _get_entry_points(packages):
46792834894SJohn Snow        for filename in maker.make(entry_point):
46892834894SJohn Snow            logger.debug("wrote console_script '%s'", filename)
46992834894SJohn Snow
47092834894SJohn Snow
4714695a22eSJohn Snowdef pkgname_from_depspec(dep_spec: str) -> str:
4724695a22eSJohn Snow    """
4734695a22eSJohn Snow    Parse package name out of a PEP-508 depspec.
4744695a22eSJohn Snow
4754695a22eSJohn Snow    See https://peps.python.org/pep-0508/#names
4764695a22eSJohn Snow    """
4774695a22eSJohn Snow    match = re.match(
4784695a22eSJohn Snow        r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE
4794695a22eSJohn Snow    )
4804695a22eSJohn Snow    if not match:
4814695a22eSJohn Snow        raise ValueError(
4824695a22eSJohn Snow            f"dep_spec '{dep_spec}'"
4834695a22eSJohn Snow            " does not appear to contain a valid package name"
4844695a22eSJohn Snow        )
4854695a22eSJohn Snow    return match.group(0)
4864695a22eSJohn Snow
4874695a22eSJohn Snow
48847a90a51SPaolo Bonzinidef _path_is_prefix(prefix: Optional[str], path: str) -> bool:
48947a90a51SPaolo Bonzini    try:
49047a90a51SPaolo Bonzini        return (
49147a90a51SPaolo Bonzini            prefix is not None and os.path.commonpath([prefix, path]) == prefix
49247a90a51SPaolo Bonzini        )
49347a90a51SPaolo Bonzini    except ValueError:
49447a90a51SPaolo Bonzini        return False
49547a90a51SPaolo Bonzini
49647a90a51SPaolo Bonzini
4973e4b6b0aSPaolo Bonzinidef _is_system_package(dist: Distribution) -> bool:
4983e4b6b0aSPaolo Bonzini    path = str(dist.locate_file("."))
4993e4b6b0aSPaolo Bonzini    return not (
50047a90a51SPaolo Bonzini        _path_is_prefix(sysconfig.get_path("purelib"), path)
50147a90a51SPaolo Bonzini        or _path_is_prefix(sysconfig.get_path("platlib"), path)
50247a90a51SPaolo Bonzini    )
50347a90a51SPaolo Bonzini
50447a90a51SPaolo Bonzini
5054695a22eSJohn Snowdef diagnose(
5064695a22eSJohn Snow    dep_spec: str,
5074695a22eSJohn Snow    online: bool,
5084695a22eSJohn Snow    wheels_dir: Optional[Union[str, Path]],
5094695a22eSJohn Snow    prog: Optional[str],
5104695a22eSJohn Snow) -> Tuple[str, bool]:
5114695a22eSJohn Snow    """
5124695a22eSJohn Snow    Offer a summary to the user as to why a package failed to be installed.
5134695a22eSJohn Snow
5144695a22eSJohn Snow    :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5'
5154695a22eSJohn Snow    :param online: Did we allow PyPI access?
5164695a22eSJohn Snow    :param prog:
5174695a22eSJohn Snow        Optionally, a shell program name that can be used as a
5184695a22eSJohn Snow        bellwether to detect if this program is installed elsewhere on
5194695a22eSJohn Snow        the system. This is used to offer advice when a program is
5204695a22eSJohn Snow        detected for a different python version.
5214695a22eSJohn Snow    :param wheels_dir:
5224695a22eSJohn Snow        Optionally, a directory that was searched for vendored packages.
5234695a22eSJohn Snow    """
5244695a22eSJohn Snow    # pylint: disable=too-many-branches
5254695a22eSJohn Snow
5264695a22eSJohn Snow    # Some errors are not particularly serious
5274695a22eSJohn Snow    bad = False
5284695a22eSJohn Snow
5294695a22eSJohn Snow    pkg_name = pkgname_from_depspec(dep_spec)
5303e4b6b0aSPaolo Bonzini    pkg_version: Optional[str] = None
5313e4b6b0aSPaolo Bonzini    try:
5323e4b6b0aSPaolo Bonzini        pkg_version = version(pkg_name)
5333e4b6b0aSPaolo Bonzini    except PackageNotFoundError:
5343e4b6b0aSPaolo Bonzini        pass
5354695a22eSJohn Snow
5364695a22eSJohn Snow    lines = []
5374695a22eSJohn Snow
5384695a22eSJohn Snow    if pkg_version:
5394695a22eSJohn Snow        lines.append(
5404695a22eSJohn Snow            f"Python package '{pkg_name}' version '{pkg_version}' was found,"
5414695a22eSJohn Snow            " but isn't suitable."
5424695a22eSJohn Snow        )
5434695a22eSJohn Snow    else:
5444695a22eSJohn Snow        lines.append(
545c673f3d0SPaolo Bonzini            f"Python package '{pkg_name}' was not found nor installed."
5464695a22eSJohn Snow        )
5474695a22eSJohn Snow
5484695a22eSJohn Snow    if wheels_dir:
5494695a22eSJohn Snow        lines.append(
5504695a22eSJohn Snow            "No suitable version found in, or failed to install from"
5514695a22eSJohn Snow            f" '{wheels_dir}'."
5524695a22eSJohn Snow        )
5534695a22eSJohn Snow        bad = True
5544695a22eSJohn Snow
5554695a22eSJohn Snow    if online:
5564695a22eSJohn Snow        lines.append("A suitable version could not be obtained from PyPI.")
5574695a22eSJohn Snow        bad = True
5584695a22eSJohn Snow    else:
5594695a22eSJohn Snow        lines.append(
5604695a22eSJohn Snow            "mkvenv was configured to operate offline and did not check PyPI."
5614695a22eSJohn Snow        )
5624695a22eSJohn Snow
5634695a22eSJohn Snow    if prog and not pkg_version:
5644695a22eSJohn Snow        which = shutil.which(prog)
5654695a22eSJohn Snow        if which:
5664695a22eSJohn Snow            if sys.base_prefix in site.PREFIXES:
5674695a22eSJohn Snow                pypath = Path(sys.executable).resolve()
5684695a22eSJohn Snow                lines.append(
5694695a22eSJohn Snow                    f"'{prog}' was detected on your system at '{which}', "
5704695a22eSJohn Snow                    f"but the Python package '{pkg_name}' was not found by "
5714695a22eSJohn Snow                    f"this Python interpreter ('{pypath}'). "
5724695a22eSJohn Snow                    f"Typically this means that '{prog}' has been installed "
5734695a22eSJohn Snow                    "against a different Python interpreter on your system."
5744695a22eSJohn Snow                )
5754695a22eSJohn Snow            else:
5764695a22eSJohn Snow                lines.append(
5774695a22eSJohn Snow                    f"'{prog}' was detected on your system at '{which}', "
5784695a22eSJohn Snow                    "but the build is using an isolated virtual environment."
5794695a22eSJohn Snow                )
5804695a22eSJohn Snow            bad = True
5814695a22eSJohn Snow
5824695a22eSJohn Snow    lines = [f" • {line}" for line in lines]
5834695a22eSJohn Snow    if bad:
5844695a22eSJohn Snow        lines.insert(0, f"Could not provide build dependency '{dep_spec}':")
5854695a22eSJohn Snow    else:
5864695a22eSJohn Snow        lines.insert(0, f"'{dep_spec}' not found:")
5874695a22eSJohn Snow    return os.linesep.join(lines), bad
5884695a22eSJohn Snow
5894695a22eSJohn Snow
590c5538eedSJohn Snowdef pip_install(
591c5538eedSJohn Snow    args: Sequence[str],
592c5538eedSJohn Snow    online: bool = False,
593c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
594c5538eedSJohn Snow) -> None:
595c5538eedSJohn Snow    """
596c5538eedSJohn Snow    Use pip to install a package or package(s) as specified in @args.
597c5538eedSJohn Snow    """
598c5538eedSJohn Snow    loud = bool(
599c5538eedSJohn Snow        os.environ.get("DEBUG")
600c5538eedSJohn Snow        or os.environ.get("GITLAB_CI")
601c5538eedSJohn Snow        or os.environ.get("V")
602c5538eedSJohn Snow    )
603c5538eedSJohn Snow
604c5538eedSJohn Snow    full_args = [
605c5538eedSJohn Snow        sys.executable,
606c5538eedSJohn Snow        "-m",
607c5538eedSJohn Snow        "pip",
608c5538eedSJohn Snow        "install",
609c5538eedSJohn Snow        "--disable-pip-version-check",
610c5538eedSJohn Snow        "-v" if loud else "-q",
611c5538eedSJohn Snow    ]
612c5538eedSJohn Snow    if not online:
613c5538eedSJohn Snow        full_args += ["--no-index"]
614c5538eedSJohn Snow    if wheels_dir:
615c5538eedSJohn Snow        full_args += ["--find-links", f"file://{str(wheels_dir)}"]
616c5538eedSJohn Snow    full_args += list(args)
617c5538eedSJohn Snow    subprocess.run(
618c5538eedSJohn Snow        full_args,
619c5538eedSJohn Snow        check=True,
620c5538eedSJohn Snow    )
621c5538eedSJohn Snow
622c5538eedSJohn Snow
6230f1ec070SPaolo Bonzinidef _make_version_constraint(info: Dict[str, str], install: bool) -> str:
6240f1ec070SPaolo Bonzini    """
6250f1ec070SPaolo Bonzini    Construct the version constraint part of a PEP 508 dependency
6260f1ec070SPaolo Bonzini    specification (for example '>=0.61.5') from the accepted and
6270f1ec070SPaolo Bonzini    installed keys of the provided dictionary.
6280f1ec070SPaolo Bonzini
6290f1ec070SPaolo Bonzini    :param info: A dictionary corresponding to a TOML key-value list.
6300f1ec070SPaolo Bonzini    :param install: True generates install constraints, False generates
6310f1ec070SPaolo Bonzini        presence constraints
6320f1ec070SPaolo Bonzini    """
6330f1ec070SPaolo Bonzini    if install and "installed" in info:
6340f1ec070SPaolo Bonzini        return "==" + info["installed"]
6350f1ec070SPaolo Bonzini
6360f1ec070SPaolo Bonzini    dep_spec = info.get("accepted", "")
6370f1ec070SPaolo Bonzini    dep_spec = dep_spec.strip()
6380f1ec070SPaolo Bonzini    # Double check that they didn't just use a version number
6390f1ec070SPaolo Bonzini    if dep_spec and dep_spec[0] not in "!~><=(":
6400f1ec070SPaolo Bonzini        raise Ouch(
6410f1ec070SPaolo Bonzini            "invalid dependency specifier " + dep_spec + " in dependency file"
6420f1ec070SPaolo Bonzini        )
6430f1ec070SPaolo Bonzini
6440f1ec070SPaolo Bonzini    return dep_spec
6450f1ec070SPaolo Bonzini
6460f1ec070SPaolo Bonzini
6474695a22eSJohn Snowdef _do_ensure(
6480f1ec070SPaolo Bonzini    group: Dict[str, Dict[str, str]],
649c5538eedSJohn Snow    online: bool = False,
650c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
651d37c21b5SPaolo Bonzini) -> Optional[Tuple[str, bool]]:
652c5538eedSJohn Snow    """
6530f1ec070SPaolo Bonzini    Use pip to ensure we have the packages specified in @group.
654c5538eedSJohn Snow
6550f1ec070SPaolo Bonzini    If the packages are already installed, do nothing. If online and
656c5538eedSJohn Snow    wheels_dir are both provided, prefer packages found in wheels_dir
657c5538eedSJohn Snow    first before connecting to PyPI.
658c5538eedSJohn Snow
6590f1ec070SPaolo Bonzini    :param group: A dictionary of dictionaries, corresponding to a
6600f1ec070SPaolo Bonzini        section in a pythondeps.toml file.
661c5538eedSJohn Snow    :param online: If True, fall back to PyPI.
662c5538eedSJohn Snow    :param wheels_dir: If specified, search this path for packages.
663c5538eedSJohn Snow    """
664c5538eedSJohn Snow    absent = []
66592834894SJohn Snow    present = []
66667b9a83dSPaolo Bonzini    canary = None
6670f1ec070SPaolo Bonzini    for name, info in group.items():
6680f1ec070SPaolo Bonzini        constraint = _make_version_constraint(info, False)
6690f1ec070SPaolo Bonzini        matcher = distlib.version.LegacyMatcher(name + constraint)
67071ed611cSPaolo Bonzini        print(f"mkvenv: checking for {matcher}", file=sys.stderr)
6713e4b6b0aSPaolo Bonzini
6723e4b6b0aSPaolo Bonzini        dist: Optional[Distribution] = None
6733e4b6b0aSPaolo Bonzini        try:
6743e4b6b0aSPaolo Bonzini            dist = distribution(matcher.name)
6753e4b6b0aSPaolo Bonzini        except PackageNotFoundError:
6763e4b6b0aSPaolo Bonzini            pass
6773e4b6b0aSPaolo Bonzini
67847a90a51SPaolo Bonzini        if (
6793e4b6b0aSPaolo Bonzini            dist is None
68047a90a51SPaolo Bonzini            # Always pass installed package to pip, so that they can be
68147a90a51SPaolo Bonzini            # updated if the requested version changes
6823e4b6b0aSPaolo Bonzini            or not _is_system_package(dist)
6833e4b6b0aSPaolo Bonzini            or not matcher.match(distlib.version.LegacyVersion(dist.version))
684c673f3d0SPaolo Bonzini        ):
6850f1ec070SPaolo Bonzini            absent.append(name + _make_version_constraint(info, True))
6860f1ec070SPaolo Bonzini            if len(absent) == 1:
6870f1ec070SPaolo Bonzini                canary = info.get("canary", None)
688c5538eedSJohn Snow        else:
6893e4b6b0aSPaolo Bonzini            logger.info("found %s %s", name, dist.version)
6900f1ec070SPaolo Bonzini            present.append(name)
69192834894SJohn Snow
69292834894SJohn Snow    if present:
69392834894SJohn Snow        generate_console_scripts(present)
694c5538eedSJohn Snow
695c5538eedSJohn Snow    if absent:
696d37c21b5SPaolo Bonzini        if online or wheels_dir:
697c5538eedSJohn Snow            # Some packages are missing or aren't a suitable version,
698c5538eedSJohn Snow            # install a suitable (possibly vendored) package.
699c5538eedSJohn Snow            print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr)
700d37c21b5SPaolo Bonzini            try:
701c5538eedSJohn Snow                pip_install(args=absent, online=online, wheels_dir=wheels_dir)
702d37c21b5SPaolo Bonzini                return None
703d37c21b5SPaolo Bonzini            except subprocess.CalledProcessError:
704d37c21b5SPaolo Bonzini                pass
705d37c21b5SPaolo Bonzini
706d37c21b5SPaolo Bonzini        return diagnose(
707d37c21b5SPaolo Bonzini            absent[0],
708d37c21b5SPaolo Bonzini            online,
709d37c21b5SPaolo Bonzini            wheels_dir,
71067b9a83dSPaolo Bonzini            canary,
711d37c21b5SPaolo Bonzini        )
712d37c21b5SPaolo Bonzini
713d37c21b5SPaolo Bonzini    return None
714c5538eedSJohn Snow
715c5538eedSJohn Snow
71671ed611cSPaolo Bonzinidef _parse_groups(file: str) -> Dict[str, Dict[str, Any]]:
71771ed611cSPaolo Bonzini    if not HAVE_TOMLLIB:
71871ed611cSPaolo Bonzini        if sys.version_info < (3, 11):
71971ed611cSPaolo Bonzini            raise Ouch("found no usable tomli, please install it")
72071ed611cSPaolo Bonzini
72171ed611cSPaolo Bonzini        raise Ouch(
72271ed611cSPaolo Bonzini            "Python >=3.11 does not have tomllib... what have you done!?"
72371ed611cSPaolo Bonzini        )
72471ed611cSPaolo Bonzini
72571ed611cSPaolo Bonzini    # Use loads() to support both tomli v1.2.x (Ubuntu 22.04,
72671ed611cSPaolo Bonzini    # Debian bullseye-backports) and v2.0.x
72771ed611cSPaolo Bonzini    with open(file, "r", encoding="ascii") as depfile:
72871ed611cSPaolo Bonzini        contents = depfile.read()
72971ed611cSPaolo Bonzini        return tomllib.loads(contents)  # type: ignore
73071ed611cSPaolo Bonzini
73171ed611cSPaolo Bonzini
73271ed611cSPaolo Bonzinidef ensure_group(
73371ed611cSPaolo Bonzini    file: str,
73471ed611cSPaolo Bonzini    groups: Sequence[str],
73571ed611cSPaolo Bonzini    online: bool = False,
73671ed611cSPaolo Bonzini    wheels_dir: Optional[Union[str, Path]] = None,
73771ed611cSPaolo Bonzini) -> None:
73871ed611cSPaolo Bonzini    """
73971ed611cSPaolo Bonzini    Use pip to ensure we have the package specified by @dep_specs.
74071ed611cSPaolo Bonzini
74171ed611cSPaolo Bonzini    If the package is already installed, do nothing. If online and
74271ed611cSPaolo Bonzini    wheels_dir are both provided, prefer packages found in wheels_dir
74371ed611cSPaolo Bonzini    first before connecting to PyPI.
74471ed611cSPaolo Bonzini
74571ed611cSPaolo Bonzini    :param dep_specs:
74671ed611cSPaolo Bonzini        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
74771ed611cSPaolo Bonzini    :param online: If True, fall back to PyPI.
74871ed611cSPaolo Bonzini    :param wheels_dir: If specified, search this path for packages.
74971ed611cSPaolo Bonzini    """
75071ed611cSPaolo Bonzini
75171ed611cSPaolo Bonzini    if not HAVE_DISTLIB:
75271ed611cSPaolo Bonzini        raise Ouch("found no usable distlib, please install it")
75371ed611cSPaolo Bonzini
75471ed611cSPaolo Bonzini    parsed_deps = _parse_groups(file)
75571ed611cSPaolo Bonzini
75671ed611cSPaolo Bonzini    to_install: Dict[str, Dict[str, str]] = {}
75771ed611cSPaolo Bonzini    for group in groups:
75871ed611cSPaolo Bonzini        try:
75971ed611cSPaolo Bonzini            to_install.update(parsed_deps[group])
76071ed611cSPaolo Bonzini        except KeyError as exc:
76171ed611cSPaolo Bonzini            raise Ouch(f"group {group} not defined") from exc
76271ed611cSPaolo Bonzini
76371ed611cSPaolo Bonzini    result = _do_ensure(to_install, online, wheels_dir)
76471ed611cSPaolo Bonzini    if result:
76571ed611cSPaolo Bonzini        # Well, that's not good.
76671ed611cSPaolo Bonzini        if result[1]:
76771ed611cSPaolo Bonzini            raise Ouch(result[0])
76871ed611cSPaolo Bonzini        raise SystemExit(f"\n{result[0]}\n\n")
76971ed611cSPaolo Bonzini
77071ed611cSPaolo Bonzini
771f1ad527fSJohn Snowdef post_venv_setup() -> None:
772f1ad527fSJohn Snow    """
773f1ad527fSJohn Snow    This is intended to be run *inside the venv* after it is created.
774f1ad527fSJohn Snow    """
775f1ad527fSJohn Snow    logger.debug("post_venv_setup()")
776*0a88ac96SPaolo Bonzini    # Generate a 'pip' script so the venv is usable in a normal
777f1ad527fSJohn Snow    # way from the CLI. This only happens when we inherited pip from a
778f1ad527fSJohn Snow    # parent/system-site and haven't run ensurepip in some way.
779f1ad527fSJohn Snow    generate_console_scripts(["pip"])
780f1ad527fSJohn Snow
781f1ad527fSJohn Snow
782dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None:
783dd84028fSJohn Snow    subparser = subparsers.add_parser("create", help="create a venv")
784dd84028fSJohn Snow    subparser.add_argument(
785dd84028fSJohn Snow        "target",
786dd84028fSJohn Snow        type=str,
787dd84028fSJohn Snow        action="store",
788dd84028fSJohn Snow        help="Target directory to install virtual environment into.",
789dd84028fSJohn Snow    )
790dd84028fSJohn Snow
791dd84028fSJohn Snow
792f1ad527fSJohn Snowdef _add_post_init_subcommand(subparsers: Any) -> None:
793f1ad527fSJohn Snow    subparsers.add_parser("post_init", help="post-venv initialization")
794f1ad527fSJohn Snow
795f1ad527fSJohn Snow
79671ed611cSPaolo Bonzinidef _add_ensuregroup_subcommand(subparsers: Any) -> None:
79771ed611cSPaolo Bonzini    subparser = subparsers.add_parser(
79871ed611cSPaolo Bonzini        "ensuregroup",
79971ed611cSPaolo Bonzini        help="Ensure that the specified package group is installed.",
80071ed611cSPaolo Bonzini    )
80171ed611cSPaolo Bonzini    subparser.add_argument(
80271ed611cSPaolo Bonzini        "--online",
80371ed611cSPaolo Bonzini        action="store_true",
80471ed611cSPaolo Bonzini        help="Install packages from PyPI, if necessary.",
80571ed611cSPaolo Bonzini    )
80671ed611cSPaolo Bonzini    subparser.add_argument(
80771ed611cSPaolo Bonzini        "--dir",
80871ed611cSPaolo Bonzini        type=str,
80971ed611cSPaolo Bonzini        action="store",
81071ed611cSPaolo Bonzini        help="Path to vendored packages where we may install from.",
81171ed611cSPaolo Bonzini    )
81271ed611cSPaolo Bonzini    subparser.add_argument(
81371ed611cSPaolo Bonzini        "file",
81471ed611cSPaolo Bonzini        type=str,
81571ed611cSPaolo Bonzini        action="store",
81671ed611cSPaolo Bonzini        help=("Path to a TOML file describing package groups"),
81771ed611cSPaolo Bonzini    )
81871ed611cSPaolo Bonzini    subparser.add_argument(
81971ed611cSPaolo Bonzini        "group",
82071ed611cSPaolo Bonzini        type=str,
82171ed611cSPaolo Bonzini        action="store",
82271ed611cSPaolo Bonzini        help="One or more package group names",
82371ed611cSPaolo Bonzini        nargs="+",
82471ed611cSPaolo Bonzini    )
82571ed611cSPaolo Bonzini
82671ed611cSPaolo Bonzini
827dd84028fSJohn Snowdef main() -> int:
828dd84028fSJohn Snow    """CLI interface to make_qemu_venv. See module docstring."""
829dd84028fSJohn Snow    if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
830dd84028fSJohn Snow        # You're welcome.
831dd84028fSJohn Snow        logging.basicConfig(level=logging.DEBUG)
832c5538eedSJohn Snow    else:
833c5538eedSJohn Snow        if os.environ.get("V"):
834dd84028fSJohn Snow            logging.basicConfig(level=logging.INFO)
835dd84028fSJohn Snow
836dd84028fSJohn Snow    parser = argparse.ArgumentParser(
837dd84028fSJohn Snow        prog="mkvenv",
838dd84028fSJohn Snow        description="QEMU pyvenv bootstrapping utility",
839dd84028fSJohn Snow    )
840dd84028fSJohn Snow    subparsers = parser.add_subparsers(
841dd84028fSJohn Snow        title="Commands",
842dd84028fSJohn Snow        dest="command",
84302312f1aSPaolo Bonzini        required=True,
844dd84028fSJohn Snow        metavar="command",
845dd84028fSJohn Snow        help="Description",
846dd84028fSJohn Snow    )
847dd84028fSJohn Snow
848dd84028fSJohn Snow    _add_create_subcommand(subparsers)
849f1ad527fSJohn Snow    _add_post_init_subcommand(subparsers)
85071ed611cSPaolo Bonzini    _add_ensuregroup_subcommand(subparsers)
851dd84028fSJohn Snow
852dd84028fSJohn Snow    args = parser.parse_args()
853dd84028fSJohn Snow    try:
854dd84028fSJohn Snow        if args.command == "create":
855dd84028fSJohn Snow            make_venv(
856dd84028fSJohn Snow                args.target,
857dd84028fSJohn Snow                system_site_packages=True,
858dd84028fSJohn Snow                clear=True,
859dd84028fSJohn Snow            )
860f1ad527fSJohn Snow        if args.command == "post_init":
861f1ad527fSJohn Snow            post_venv_setup()
86271ed611cSPaolo Bonzini        if args.command == "ensuregroup":
86371ed611cSPaolo Bonzini            ensure_group(
86471ed611cSPaolo Bonzini                file=args.file,
86571ed611cSPaolo Bonzini                groups=args.group,
86671ed611cSPaolo Bonzini                online=args.online,
86771ed611cSPaolo Bonzini                wheels_dir=args.dir,
86871ed611cSPaolo Bonzini            )
869dd84028fSJohn Snow        logger.debug("mkvenv.py %s: exiting", args.command)
870dd84028fSJohn Snow    except Ouch as exc:
871dd84028fSJohn Snow        print("\n*** Ouch! ***\n", file=sys.stderr)
872dd84028fSJohn Snow        print(str(exc), "\n\n", file=sys.stderr)
873dd84028fSJohn Snow        return 1
874dd84028fSJohn Snow    except SystemExit:
875dd84028fSJohn Snow        raise
876dd84028fSJohn Snow    except:  # pylint: disable=bare-except
877dd84028fSJohn Snow        logger.exception("mkvenv did not complete successfully:")
878dd84028fSJohn Snow        return 2
879dd84028fSJohn Snow    return 0
880dd84028fSJohn Snow
881dd84028fSJohn Snow
882dd84028fSJohn Snowif __name__ == "__main__":
883dd84028fSJohn Snow    sys.exit(main())
884