xref: /qemu/python/scripts/mkvenv.py (revision 02312f1af1524253d1b98f4fa6dfe23dc53f1c02)
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
16c5538eedSJohn Snow    ensure    Ensure that the specified package is installed.
17dd84028fSJohn Snow
18dd84028fSJohn Snow--------------------------------------------------
19dd84028fSJohn Snow
20dd84028fSJohn Snowusage: mkvenv create [-h] target
21dd84028fSJohn Snow
22dd84028fSJohn Snowpositional arguments:
23dd84028fSJohn Snow  target      Target directory to install virtual environment into.
24dd84028fSJohn Snow
25dd84028fSJohn Snowoptions:
26dd84028fSJohn Snow  -h, --help  show this help message and exit
27dd84028fSJohn Snow
28c5538eedSJohn Snow--------------------------------------------------
29c5538eedSJohn Snow
30f1ad527fSJohn Snowusage: mkvenv post_init [-h]
31f1ad527fSJohn Snow
32f1ad527fSJohn Snowoptions:
33f1ad527fSJohn Snow  -h, --help         show this help message and exit
34f1ad527fSJohn Snow
35f1ad527fSJohn Snow--------------------------------------------------
36f1ad527fSJohn Snow
37c5538eedSJohn Snowusage: mkvenv ensure [-h] [--online] [--dir DIR] dep_spec...
38c5538eedSJohn Snow
39c5538eedSJohn Snowpositional arguments:
40c5538eedSJohn Snow  dep_spec    PEP 508 Dependency specification, e.g. 'meson>=0.61.5'
41c5538eedSJohn Snow
42c5538eedSJohn Snowoptions:
43c5538eedSJohn Snow  -h, --help  show this help message and exit
44c5538eedSJohn Snow  --online    Install packages from PyPI, if necessary.
45c5538eedSJohn Snow  --dir DIR   Path to vendored packages where we may install from.
46c5538eedSJohn Snow
47dd84028fSJohn Snow"""
48dd84028fSJohn Snow
49dd84028fSJohn Snow# Copyright (C) 2022-2023 Red Hat, Inc.
50dd84028fSJohn Snow#
51dd84028fSJohn Snow# Authors:
52dd84028fSJohn Snow#  John Snow <jsnow@redhat.com>
53dd84028fSJohn Snow#  Paolo Bonzini <pbonzini@redhat.com>
54dd84028fSJohn Snow#
55dd84028fSJohn Snow# This work is licensed under the terms of the GNU GPL, version 2 or
56dd84028fSJohn Snow# later. See the COPYING file in the top-level directory.
57dd84028fSJohn Snow
58dd84028fSJohn Snowimport argparse
59a9dbde71SJohn Snowfrom importlib.util import find_spec
60dd84028fSJohn Snowimport logging
61dd84028fSJohn Snowimport os
62dd84028fSJohn Snowfrom pathlib import Path
634695a22eSJohn Snowimport re
644695a22eSJohn Snowimport shutil
65dee01b82SJohn Snowimport site
66dd84028fSJohn Snowimport subprocess
67dd84028fSJohn Snowimport sys
68dee01b82SJohn Snowimport sysconfig
69dd84028fSJohn Snowfrom types import SimpleNamespace
70c5538eedSJohn Snowfrom typing import (
71c5538eedSJohn Snow    Any,
7292834894SJohn Snow    Iterator,
73c5538eedSJohn Snow    Optional,
74c5538eedSJohn Snow    Sequence,
754695a22eSJohn Snow    Tuple,
76c5538eedSJohn Snow    Union,
77c5538eedSJohn Snow)
78dd84028fSJohn Snowimport venv
79c5538eedSJohn Snowimport warnings
80c5538eedSJohn Snow
8168ea6d17SJohn Snow
8268ea6d17SJohn Snow# Try to load distlib, with a fallback to pip's vendored version.
8368ea6d17SJohn Snow# HAVE_DISTLIB is checked below, just-in-time, so that mkvenv does not fail
8468ea6d17SJohn Snow# outside the venv or before a potential call to ensurepip in checkpip().
8568ea6d17SJohn SnowHAVE_DISTLIB = True
8668ea6d17SJohn Snowtry:
87c5538eedSJohn Snow    import distlib.database
8892834894SJohn Snow    import distlib.scripts
89c5538eedSJohn Snow    import distlib.version
9068ea6d17SJohn Snowexcept ImportError:
9168ea6d17SJohn Snow    try:
9268ea6d17SJohn Snow        # Reach into pip's cookie jar.  pylint and flake8 don't understand
9368ea6d17SJohn Snow        # that these imports will be used via distlib.xxx.
9468ea6d17SJohn Snow        from pip._vendor import distlib
9568ea6d17SJohn Snow        import pip._vendor.distlib.database  # noqa, pylint: disable=unused-import
9668ea6d17SJohn Snow        import pip._vendor.distlib.scripts  # noqa, pylint: disable=unused-import
9768ea6d17SJohn Snow        import pip._vendor.distlib.version  # noqa, pylint: disable=unused-import
9868ea6d17SJohn Snow    except ImportError:
9968ea6d17SJohn Snow        HAVE_DISTLIB = False
100dd84028fSJohn Snow
101dd84028fSJohn Snow# Do not add any mandatory dependencies from outside the stdlib:
102dd84028fSJohn Snow# This script *must* be usable standalone!
103dd84028fSJohn Snow
104dd84028fSJohn SnowDirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
105dd84028fSJohn Snowlogger = logging.getLogger("mkvenv")
106dd84028fSJohn Snow
107dd84028fSJohn Snow
108dee01b82SJohn Snowdef inside_a_venv() -> bool:
109dee01b82SJohn Snow    """Returns True if it is executed inside of a virtual environment."""
110dee01b82SJohn Snow    return sys.prefix != sys.base_prefix
111dee01b82SJohn Snow
112dee01b82SJohn Snow
113dd84028fSJohn Snowclass Ouch(RuntimeError):
114dd84028fSJohn Snow    """An Exception class we can't confuse with a builtin."""
115dd84028fSJohn Snow
116dd84028fSJohn Snow
117dd84028fSJohn Snowclass QemuEnvBuilder(venv.EnvBuilder):
118dd84028fSJohn Snow    """
119dd84028fSJohn Snow    An extension of venv.EnvBuilder for building QEMU's configure-time venv.
120dd84028fSJohn Snow
121dee01b82SJohn Snow    The primary difference is that it emulates a "nested" virtual
122dee01b82SJohn Snow    environment when invoked from inside of an existing virtual
123f1ad527fSJohn Snow    environment by including packages from the parent.  Also,
124f1ad527fSJohn Snow    "ensurepip" is replaced if possible with just recreating pip's
125f1ad527fSJohn Snow    console_scripts inside the virtual environment.
126dd84028fSJohn Snow
127dd84028fSJohn Snow    Parameters for base class init:
128dd84028fSJohn Snow      - system_site_packages: bool = False
129dd84028fSJohn Snow      - clear: bool = False
130dd84028fSJohn Snow      - symlinks: bool = False
131dd84028fSJohn Snow      - upgrade: bool = False
132dd84028fSJohn Snow      - with_pip: bool = False
133dd84028fSJohn Snow      - prompt: Optional[str] = None
134dd84028fSJohn Snow      - upgrade_deps: bool = False             (Since 3.9)
135dd84028fSJohn Snow    """
136dd84028fSJohn Snow
137dd84028fSJohn Snow    def __init__(self, *args: Any, **kwargs: Any) -> None:
138dd84028fSJohn Snow        logger.debug("QemuEnvBuilder.__init__(...)")
139a9dbde71SJohn Snow
140dee01b82SJohn Snow        # For nested venv emulation:
141dee01b82SJohn Snow        self.use_parent_packages = False
142dee01b82SJohn Snow        if inside_a_venv():
143dee01b82SJohn Snow            # Include parent packages only if we're in a venv and
144dee01b82SJohn Snow            # system_site_packages was True.
145dee01b82SJohn Snow            self.use_parent_packages = kwargs.pop(
146dee01b82SJohn Snow                "system_site_packages", False
147dee01b82SJohn Snow            )
148dee01b82SJohn Snow            # Include system_site_packages only when the parent,
149dee01b82SJohn Snow            # The venv we are currently in, also does so.
150dee01b82SJohn Snow            kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES
151dee01b82SJohn Snow
152f1ad527fSJohn Snow        # ensurepip is slow: venv creation can be very fast for cases where
153f1ad527fSJohn Snow        # we allow the use of system_site_packages. Therefore, ensurepip is
154f1ad527fSJohn Snow        # replaced with our own script generation once the virtual environment
155f1ad527fSJohn Snow        # is setup.
156f1ad527fSJohn Snow        self.want_pip = kwargs.get("with_pip", False)
157f1ad527fSJohn Snow        if self.want_pip:
158f1ad527fSJohn Snow            if (
159f1ad527fSJohn Snow                kwargs.get("system_site_packages", False)
160f1ad527fSJohn Snow                and not need_ensurepip()
161f1ad527fSJohn Snow            ):
162f1ad527fSJohn Snow                kwargs["with_pip"] = False
163f1ad527fSJohn Snow            else:
164c8049626SJohn Snow                check_ensurepip(suggest_remedy=True)
165a9dbde71SJohn Snow
166dd84028fSJohn Snow        super().__init__(*args, **kwargs)
167dd84028fSJohn Snow
168dd84028fSJohn Snow        # Make the context available post-creation:
169dd84028fSJohn Snow        self._context: Optional[SimpleNamespace] = None
170dd84028fSJohn Snow
171dee01b82SJohn Snow    def get_parent_libpath(self) -> Optional[str]:
172dee01b82SJohn Snow        """Return the libpath of the parent venv, if applicable."""
173dee01b82SJohn Snow        if self.use_parent_packages:
174dee01b82SJohn Snow            return sysconfig.get_path("purelib")
175dee01b82SJohn Snow        return None
176dee01b82SJohn Snow
177dee01b82SJohn Snow    @staticmethod
178dee01b82SJohn Snow    def compute_venv_libpath(context: SimpleNamespace) -> str:
179dee01b82SJohn Snow        """
180dee01b82SJohn Snow        Compatibility wrapper for context.lib_path for Python < 3.12
181dee01b82SJohn Snow        """
182dee01b82SJohn Snow        # Python 3.12+, not strictly necessary because it's documented
183dee01b82SJohn Snow        # to be the same as 3.10 code below:
184dee01b82SJohn Snow        if sys.version_info >= (3, 12):
185dee01b82SJohn Snow            return context.lib_path
186dee01b82SJohn Snow
187dee01b82SJohn Snow        # Python 3.10+
188dee01b82SJohn Snow        if "venv" in sysconfig.get_scheme_names():
189dee01b82SJohn Snow            lib_path = sysconfig.get_path(
190dee01b82SJohn Snow                "purelib", scheme="venv", vars={"base": context.env_dir}
191dee01b82SJohn Snow            )
192dee01b82SJohn Snow            assert lib_path is not None
193dee01b82SJohn Snow            return lib_path
194dee01b82SJohn Snow
195dee01b82SJohn Snow        # For Python <= 3.9 we need to hardcode this. Fortunately the
196dee01b82SJohn Snow        # code below was the same in Python 3.6-3.10, so there is only
197dee01b82SJohn Snow        # one case.
198dee01b82SJohn Snow        if sys.platform == "win32":
199dee01b82SJohn Snow            return os.path.join(context.env_dir, "Lib", "site-packages")
200dee01b82SJohn Snow        return os.path.join(
201dee01b82SJohn Snow            context.env_dir,
202dee01b82SJohn Snow            "lib",
203dee01b82SJohn Snow            "python%d.%d" % sys.version_info[:2],
204dee01b82SJohn Snow            "site-packages",
205dee01b82SJohn Snow        )
206dee01b82SJohn Snow
207dd84028fSJohn Snow    def ensure_directories(self, env_dir: DirType) -> SimpleNamespace:
208dd84028fSJohn Snow        logger.debug("ensure_directories(env_dir=%s)", env_dir)
209dd84028fSJohn Snow        self._context = super().ensure_directories(env_dir)
210dd84028fSJohn Snow        return self._context
211dd84028fSJohn Snow
212dee01b82SJohn Snow    def create(self, env_dir: DirType) -> None:
213dee01b82SJohn Snow        logger.debug("create(env_dir=%s)", env_dir)
214dee01b82SJohn Snow        super().create(env_dir)
215dee01b82SJohn Snow        assert self._context is not None
216dee01b82SJohn Snow        self.post_post_setup(self._context)
217dee01b82SJohn Snow
218dee01b82SJohn Snow    def post_post_setup(self, context: SimpleNamespace) -> None:
219dee01b82SJohn Snow        """
220dee01b82SJohn Snow        The final, final hook. Enter the venv and run commands inside of it.
221dee01b82SJohn Snow        """
222dee01b82SJohn Snow        if self.use_parent_packages:
223dee01b82SJohn Snow            # We're inside of a venv and we want to include the parent
224dee01b82SJohn Snow            # venv's packages.
225dee01b82SJohn Snow            parent_libpath = self.get_parent_libpath()
226dee01b82SJohn Snow            assert parent_libpath is not None
227dee01b82SJohn Snow            logger.debug("parent_libpath: %s", parent_libpath)
228dee01b82SJohn Snow
229dee01b82SJohn Snow            our_libpath = self.compute_venv_libpath(context)
230dee01b82SJohn Snow            logger.debug("our_libpath: %s", our_libpath)
231dee01b82SJohn Snow
232dee01b82SJohn Snow            pth_file = os.path.join(our_libpath, "nested.pth")
233dee01b82SJohn Snow            with open(pth_file, "w", encoding="UTF-8") as file:
234dee01b82SJohn Snow                file.write(parent_libpath + os.linesep)
235dee01b82SJohn Snow
236f1ad527fSJohn Snow        if self.want_pip:
237f1ad527fSJohn Snow            args = [
238f1ad527fSJohn Snow                context.env_exe,
239f1ad527fSJohn Snow                __file__,
240f1ad527fSJohn Snow                "post_init",
241f1ad527fSJohn Snow            ]
242f1ad527fSJohn Snow            subprocess.run(args, check=True)
243f1ad527fSJohn Snow
244dd84028fSJohn Snow    def get_value(self, field: str) -> str:
245dd84028fSJohn Snow        """
246dd84028fSJohn Snow        Get a string value from the context namespace after a call to build.
247dd84028fSJohn Snow
248dd84028fSJohn Snow        For valid field names, see:
249dd84028fSJohn Snow        https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories
250dd84028fSJohn Snow        """
251dd84028fSJohn Snow        ret = getattr(self._context, field)
252dd84028fSJohn Snow        assert isinstance(ret, str)
253dd84028fSJohn Snow        return ret
254dd84028fSJohn Snow
255dd84028fSJohn Snow
256f1ad527fSJohn Snowdef need_ensurepip() -> bool:
257f1ad527fSJohn Snow    """
258f1ad527fSJohn Snow    Tests for the presence of setuptools and pip.
259f1ad527fSJohn Snow
260f1ad527fSJohn Snow    :return: `True` if we do not detect both packages.
261f1ad527fSJohn Snow    """
262f1ad527fSJohn Snow    # Don't try to actually import them, it's fraught with danger:
263f1ad527fSJohn Snow    # https://github.com/pypa/setuptools/issues/2993
264f1ad527fSJohn Snow    if find_spec("setuptools") and find_spec("pip"):
265f1ad527fSJohn Snow        return False
266f1ad527fSJohn Snow    return True
267f1ad527fSJohn Snow
268f1ad527fSJohn Snow
269c8049626SJohn Snowdef check_ensurepip(prefix: str = "", suggest_remedy: bool = False) -> None:
270a9dbde71SJohn Snow    """
271a9dbde71SJohn Snow    Check that we have ensurepip.
272a9dbde71SJohn Snow
273a9dbde71SJohn Snow    Raise a fatal exception with a helpful hint if it isn't available.
274a9dbde71SJohn Snow    """
275a9dbde71SJohn Snow    if not find_spec("ensurepip"):
276a9dbde71SJohn Snow        msg = (
277a9dbde71SJohn Snow            "Python's ensurepip module is not found.\n"
278a9dbde71SJohn Snow            "It's normally part of the Python standard library, "
279a9dbde71SJohn Snow            "maybe your distribution packages it separately?\n"
280c8049626SJohn Snow            "(Debian puts ensurepip in its python3-venv package.)\n"
281c8049626SJohn Snow        )
282c8049626SJohn Snow        if suggest_remedy:
283c8049626SJohn Snow            msg += (
284a9dbde71SJohn Snow                "Either install ensurepip, or alleviate the need for it in the"
285a9dbde71SJohn Snow                " first place by installing pip and setuptools for "
286a9dbde71SJohn Snow                f"'{sys.executable}'.\n"
287a9dbde71SJohn Snow            )
288c8049626SJohn Snow        raise Ouch(prefix + msg)
289a9dbde71SJohn Snow
290a9dbde71SJohn Snow    # ensurepip uses pyexpat, which can also go missing on us:
291a9dbde71SJohn Snow    if not find_spec("pyexpat"):
292a9dbde71SJohn Snow        msg = (
293a9dbde71SJohn Snow            "Python's pyexpat module is not found.\n"
294a9dbde71SJohn Snow            "It's normally part of the Python standard library, "
295a9dbde71SJohn Snow            "maybe your distribution packages it separately?\n"
296c8049626SJohn Snow            "(NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)\n"
297c8049626SJohn Snow        )
298c8049626SJohn Snow        if suggest_remedy:
299c8049626SJohn Snow            msg += (
300a9dbde71SJohn Snow                "Either install pyexpat, or alleviate the need for it in the "
301a9dbde71SJohn Snow                "first place by installing pip and setuptools for "
302c8049626SJohn Snow                f"'{sys.executable}'.\n"
303a9dbde71SJohn Snow            )
304c8049626SJohn Snow        raise Ouch(prefix + msg)
305a9dbde71SJohn Snow
306a9dbde71SJohn Snow
307dd84028fSJohn Snowdef make_venv(  # pylint: disable=too-many-arguments
308dd84028fSJohn Snow    env_dir: Union[str, Path],
309dd84028fSJohn Snow    system_site_packages: bool = False,
310dd84028fSJohn Snow    clear: bool = True,
311dd84028fSJohn Snow    symlinks: Optional[bool] = None,
312dd84028fSJohn Snow    with_pip: bool = True,
313dd84028fSJohn Snow) -> None:
314dd84028fSJohn Snow    """
315dd84028fSJohn Snow    Create a venv using `QemuEnvBuilder`.
316dd84028fSJohn Snow
317dd84028fSJohn Snow    This is analogous to the `venv.create` module-level convenience
318dd84028fSJohn Snow    function that is part of the Python stdblib, except it uses
319dd84028fSJohn Snow    `QemuEnvBuilder` instead.
320dd84028fSJohn Snow
321dd84028fSJohn Snow    :param env_dir: The directory to create/install to.
322dd84028fSJohn Snow    :param system_site_packages:
323dd84028fSJohn Snow        Allow inheriting packages from the system installation.
324dd84028fSJohn Snow    :param clear: When True, fully remove any prior venv and files.
325dd84028fSJohn Snow    :param symlinks:
326dd84028fSJohn Snow        Whether to use symlinks to the target interpreter or not. If
327dd84028fSJohn Snow        left unspecified, it will use symlinks except on Windows to
328dd84028fSJohn Snow        match behavior with the "venv" CLI tool.
329dd84028fSJohn Snow    :param with_pip:
330dd84028fSJohn Snow        Whether to install "pip" binaries or not.
331dd84028fSJohn Snow    """
332dd84028fSJohn Snow    logger.debug(
333dd84028fSJohn Snow        "%s: make_venv(env_dir=%s, system_site_packages=%s, "
334dd84028fSJohn Snow        "clear=%s, symlinks=%s, with_pip=%s)",
335dd84028fSJohn Snow        __file__,
336dd84028fSJohn Snow        str(env_dir),
337dd84028fSJohn Snow        system_site_packages,
338dd84028fSJohn Snow        clear,
339dd84028fSJohn Snow        symlinks,
340dd84028fSJohn Snow        with_pip,
341dd84028fSJohn Snow    )
342dd84028fSJohn Snow
343dd84028fSJohn Snow    if symlinks is None:
344dd84028fSJohn Snow        # Default behavior of standard venv CLI
345dd84028fSJohn Snow        symlinks = os.name != "nt"
346dd84028fSJohn Snow
347dd84028fSJohn Snow    builder = QemuEnvBuilder(
348dd84028fSJohn Snow        system_site_packages=system_site_packages,
349dd84028fSJohn Snow        clear=clear,
350dd84028fSJohn Snow        symlinks=symlinks,
351dd84028fSJohn Snow        with_pip=with_pip,
352dd84028fSJohn Snow    )
353dd84028fSJohn Snow
354dd84028fSJohn Snow    style = "non-isolated" if builder.system_site_packages else "isolated"
355dee01b82SJohn Snow    nested = ""
356dee01b82SJohn Snow    if builder.use_parent_packages:
357dee01b82SJohn Snow        nested = f"(with packages from '{builder.get_parent_libpath()}') "
358dd84028fSJohn Snow    print(
359dd84028fSJohn Snow        f"mkvenv: Creating {style} virtual environment"
360dee01b82SJohn Snow        f" {nested}at '{str(env_dir)}'",
361dd84028fSJohn Snow        file=sys.stderr,
362dd84028fSJohn Snow    )
363dd84028fSJohn Snow
364dd84028fSJohn Snow    try:
365dd84028fSJohn Snow        logger.debug("Invoking builder.create()")
366dd84028fSJohn Snow        try:
367dd84028fSJohn Snow            builder.create(str(env_dir))
368dd84028fSJohn Snow        except SystemExit as exc:
369dd84028fSJohn Snow            # Some versions of the venv module raise SystemExit; *nasty*!
370dd84028fSJohn Snow            # We want the exception that prompted it. It might be a subprocess
371dd84028fSJohn Snow            # error that has output we *really* want to see.
372dd84028fSJohn Snow            logger.debug("Intercepted SystemExit from EnvBuilder.create()")
373dd84028fSJohn Snow            raise exc.__cause__ or exc.__context__ or exc
374dd84028fSJohn Snow        logger.debug("builder.create() finished")
375dd84028fSJohn Snow    except subprocess.CalledProcessError as exc:
376dd84028fSJohn Snow        logger.error("mkvenv subprocess failed:")
377dd84028fSJohn Snow        logger.error("cmd: %s", exc.cmd)
378dd84028fSJohn Snow        logger.error("returncode: %d", exc.returncode)
379dd84028fSJohn Snow
380dd84028fSJohn Snow        def _stringify(data: Union[str, bytes]) -> str:
381dd84028fSJohn Snow            if isinstance(data, bytes):
382dd84028fSJohn Snow                return data.decode()
383dd84028fSJohn Snow            return data
384dd84028fSJohn Snow
385dd84028fSJohn Snow        lines = []
386dd84028fSJohn Snow        if exc.stdout:
387dd84028fSJohn Snow            lines.append("========== stdout ==========")
388dd84028fSJohn Snow            lines.append(_stringify(exc.stdout))
389dd84028fSJohn Snow            lines.append("============================")
390dd84028fSJohn Snow        if exc.stderr:
391dd84028fSJohn Snow            lines.append("========== stderr ==========")
392dd84028fSJohn Snow            lines.append(_stringify(exc.stderr))
393dd84028fSJohn Snow            lines.append("============================")
394dd84028fSJohn Snow        if lines:
395dd84028fSJohn Snow            logger.error(os.linesep.join(lines))
396dd84028fSJohn Snow
397dd84028fSJohn Snow        raise Ouch("VENV creation subprocess failed.") from exc
398dd84028fSJohn Snow
399dd84028fSJohn Snow    # print the python executable to stdout for configure.
400dd84028fSJohn Snow    print(builder.get_value("env_exe"))
401dd84028fSJohn Snow
402dd84028fSJohn Snow
40392834894SJohn Snowdef _gen_importlib(packages: Sequence[str]) -> Iterator[str]:
40492834894SJohn Snow    # pylint: disable=import-outside-toplevel
40592834894SJohn Snow    # pylint: disable=no-name-in-module
40692834894SJohn Snow    # pylint: disable=import-error
40792834894SJohn Snow    try:
40892834894SJohn Snow        # First preference: Python 3.8+ stdlib
40992834894SJohn Snow        from importlib.metadata import (  # type: ignore
41092834894SJohn Snow            PackageNotFoundError,
41192834894SJohn Snow            distribution,
41292834894SJohn Snow        )
41392834894SJohn Snow    except ImportError as exc:
41492834894SJohn Snow        logger.debug("%s", str(exc))
41592834894SJohn Snow        # Second preference: Commonly available PyPI backport
41692834894SJohn Snow        from importlib_metadata import (  # type: ignore
41792834894SJohn Snow            PackageNotFoundError,
41892834894SJohn Snow            distribution,
41992834894SJohn Snow        )
42092834894SJohn Snow
42192834894SJohn Snow    def _generator() -> Iterator[str]:
42292834894SJohn Snow        for package in packages:
42392834894SJohn Snow            try:
42492834894SJohn Snow                entry_points = distribution(package).entry_points
42592834894SJohn Snow            except PackageNotFoundError:
42692834894SJohn Snow                continue
42792834894SJohn Snow
42892834894SJohn Snow            # The EntryPoints type is only available in 3.10+,
42992834894SJohn Snow            # treat this as a vanilla list and filter it ourselves.
43092834894SJohn Snow            entry_points = filter(
43192834894SJohn Snow                lambda ep: ep.group == "console_scripts", entry_points
43292834894SJohn Snow            )
43392834894SJohn Snow
43492834894SJohn Snow            for entry_point in entry_points:
43592834894SJohn Snow                yield f"{entry_point.name} = {entry_point.value}"
43692834894SJohn Snow
43792834894SJohn Snow    return _generator()
43892834894SJohn Snow
43992834894SJohn Snow
44092834894SJohn Snowdef _gen_pkg_resources(packages: Sequence[str]) -> Iterator[str]:
44192834894SJohn Snow    # pylint: disable=import-outside-toplevel
44292834894SJohn Snow    # Bundled with setuptools; has a good chance of being available.
44392834894SJohn Snow    import pkg_resources
44492834894SJohn Snow
44592834894SJohn Snow    def _generator() -> Iterator[str]:
44692834894SJohn Snow        for package in packages:
44792834894SJohn Snow            try:
44892834894SJohn Snow                eps = pkg_resources.get_entry_map(package, "console_scripts")
44992834894SJohn Snow            except pkg_resources.DistributionNotFound:
45092834894SJohn Snow                continue
45192834894SJohn Snow
45292834894SJohn Snow            for entry_point in eps.values():
45392834894SJohn Snow                yield str(entry_point)
45492834894SJohn Snow
45592834894SJohn Snow    return _generator()
45692834894SJohn Snow
45792834894SJohn Snow
45892834894SJohn Snowdef generate_console_scripts(
45992834894SJohn Snow    packages: Sequence[str],
46092834894SJohn Snow    python_path: Optional[str] = None,
46192834894SJohn Snow    bin_path: Optional[str] = None,
46292834894SJohn Snow) -> None:
46392834894SJohn Snow    """
46492834894SJohn Snow    Generate script shims for console_script entry points in @packages.
46592834894SJohn Snow    """
46692834894SJohn Snow    if python_path is None:
46792834894SJohn Snow        python_path = sys.executable
46892834894SJohn Snow    if bin_path is None:
46992834894SJohn Snow        bin_path = sysconfig.get_path("scripts")
47092834894SJohn Snow        assert bin_path is not None
47192834894SJohn Snow
47292834894SJohn Snow    logger.debug(
47392834894SJohn Snow        "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)",
47492834894SJohn Snow        packages,
47592834894SJohn Snow        python_path,
47692834894SJohn Snow        bin_path,
47792834894SJohn Snow    )
47892834894SJohn Snow
47992834894SJohn Snow    if not packages:
48092834894SJohn Snow        return
48192834894SJohn Snow
48292834894SJohn Snow    def _get_entry_points() -> Iterator[str]:
48392834894SJohn Snow        """Python 3.7 compatibility shim for iterating entry points."""
48492834894SJohn Snow        # Python 3.8+, or Python 3.7 with importlib_metadata installed.
48592834894SJohn Snow        try:
48692834894SJohn Snow            return _gen_importlib(packages)
48792834894SJohn Snow        except ImportError as exc:
48892834894SJohn Snow            logger.debug("%s", str(exc))
48992834894SJohn Snow
49092834894SJohn Snow        # Python 3.7 with setuptools installed.
49192834894SJohn Snow        try:
49292834894SJohn Snow            return _gen_pkg_resources(packages)
49392834894SJohn Snow        except ImportError as exc:
49492834894SJohn Snow            logger.debug("%s", str(exc))
49592834894SJohn Snow            raise Ouch(
49692834894SJohn Snow                "Neither importlib.metadata nor pkg_resources found, "
49792834894SJohn Snow                "can't generate console script shims.\n"
49892834894SJohn Snow                "Use Python 3.8+, or install importlib-metadata or setuptools."
49992834894SJohn Snow            ) from exc
50092834894SJohn Snow
50192834894SJohn Snow    maker = distlib.scripts.ScriptMaker(None, bin_path)
50292834894SJohn Snow    maker.variants = {""}
50392834894SJohn Snow    maker.clobber = False
50492834894SJohn Snow
50592834894SJohn Snow    for entry_point in _get_entry_points():
50692834894SJohn Snow        for filename in maker.make(entry_point):
50792834894SJohn Snow            logger.debug("wrote console_script '%s'", filename)
50892834894SJohn Snow
50992834894SJohn Snow
510c8049626SJohn Snowdef checkpip() -> bool:
511c8049626SJohn Snow    """
512c8049626SJohn Snow    Debian10 has a pip that's broken when used inside of a virtual environment.
513c8049626SJohn Snow
514c8049626SJohn Snow    We try to detect and correct that case here.
515c8049626SJohn Snow    """
516c8049626SJohn Snow    try:
517c8049626SJohn Snow        # pylint: disable=import-outside-toplevel,unused-import,import-error
518c8049626SJohn Snow        # pylint: disable=redefined-outer-name
519c8049626SJohn Snow        import pip._internal  # type: ignore  # noqa: F401
520c8049626SJohn Snow
521c8049626SJohn Snow        logger.debug("pip appears to be working correctly.")
522c8049626SJohn Snow        return False
523c8049626SJohn Snow    except ModuleNotFoundError as exc:
524c8049626SJohn Snow        if exc.name == "pip._internal":
525c8049626SJohn Snow            # Uh, fair enough. They did say "internal".
526c8049626SJohn Snow            # Let's just assume it's fine.
527c8049626SJohn Snow            return False
528c8049626SJohn Snow        logger.warning("pip appears to be malfunctioning: %s", str(exc))
529c8049626SJohn Snow
530c8049626SJohn Snow    check_ensurepip("pip appears to be non-functional, and ")
531c8049626SJohn Snow
532c8049626SJohn Snow    logger.debug("Attempting to repair pip ...")
533c8049626SJohn Snow    subprocess.run(
534c8049626SJohn Snow        (sys.executable, "-m", "ensurepip"),
535c8049626SJohn Snow        stdout=subprocess.DEVNULL,
536c8049626SJohn Snow        check=True,
537c8049626SJohn Snow    )
538c8049626SJohn Snow    logger.debug("Pip is now (hopefully) repaired!")
539c8049626SJohn Snow    return True
540c8049626SJohn Snow
541c8049626SJohn Snow
5424695a22eSJohn Snowdef pkgname_from_depspec(dep_spec: str) -> str:
5434695a22eSJohn Snow    """
5444695a22eSJohn Snow    Parse package name out of a PEP-508 depspec.
5454695a22eSJohn Snow
5464695a22eSJohn Snow    See https://peps.python.org/pep-0508/#names
5474695a22eSJohn Snow    """
5484695a22eSJohn Snow    match = re.match(
5494695a22eSJohn Snow        r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE
5504695a22eSJohn Snow    )
5514695a22eSJohn Snow    if not match:
5524695a22eSJohn Snow        raise ValueError(
5534695a22eSJohn Snow            f"dep_spec '{dep_spec}'"
5544695a22eSJohn Snow            " does not appear to contain a valid package name"
5554695a22eSJohn Snow        )
5564695a22eSJohn Snow    return match.group(0)
5574695a22eSJohn Snow
5584695a22eSJohn Snow
5594695a22eSJohn Snowdef diagnose(
5604695a22eSJohn Snow    dep_spec: str,
5614695a22eSJohn Snow    online: bool,
5624695a22eSJohn Snow    wheels_dir: Optional[Union[str, Path]],
5634695a22eSJohn Snow    prog: Optional[str],
5644695a22eSJohn Snow) -> Tuple[str, bool]:
5654695a22eSJohn Snow    """
5664695a22eSJohn Snow    Offer a summary to the user as to why a package failed to be installed.
5674695a22eSJohn Snow
5684695a22eSJohn Snow    :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5'
5694695a22eSJohn Snow    :param online: Did we allow PyPI access?
5704695a22eSJohn Snow    :param prog:
5714695a22eSJohn Snow        Optionally, a shell program name that can be used as a
5724695a22eSJohn Snow        bellwether to detect if this program is installed elsewhere on
5734695a22eSJohn Snow        the system. This is used to offer advice when a program is
5744695a22eSJohn Snow        detected for a different python version.
5754695a22eSJohn Snow    :param wheels_dir:
5764695a22eSJohn Snow        Optionally, a directory that was searched for vendored packages.
5774695a22eSJohn Snow    """
5784695a22eSJohn Snow    # pylint: disable=too-many-branches
5794695a22eSJohn Snow
5804695a22eSJohn Snow    # Some errors are not particularly serious
5814695a22eSJohn Snow    bad = False
5824695a22eSJohn Snow
5834695a22eSJohn Snow    pkg_name = pkgname_from_depspec(dep_spec)
5844695a22eSJohn Snow    pkg_version = None
5854695a22eSJohn Snow
5864695a22eSJohn Snow    has_importlib = False
5874695a22eSJohn Snow    try:
5884695a22eSJohn Snow        # Python 3.8+ stdlib
5894695a22eSJohn Snow        # pylint: disable=import-outside-toplevel
5904695a22eSJohn Snow        # pylint: disable=no-name-in-module
5914695a22eSJohn Snow        # pylint: disable=import-error
5924695a22eSJohn Snow        from importlib.metadata import (  # type: ignore
5934695a22eSJohn Snow            PackageNotFoundError,
5944695a22eSJohn Snow            version,
5954695a22eSJohn Snow        )
5964695a22eSJohn Snow
5974695a22eSJohn Snow        has_importlib = True
5984695a22eSJohn Snow        try:
5994695a22eSJohn Snow            pkg_version = version(pkg_name)
6004695a22eSJohn Snow        except PackageNotFoundError:
6014695a22eSJohn Snow            pass
6024695a22eSJohn Snow    except ModuleNotFoundError:
6034695a22eSJohn Snow        pass
6044695a22eSJohn Snow
6054695a22eSJohn Snow    lines = []
6064695a22eSJohn Snow
6074695a22eSJohn Snow    if pkg_version:
6084695a22eSJohn Snow        lines.append(
6094695a22eSJohn Snow            f"Python package '{pkg_name}' version '{pkg_version}' was found,"
6104695a22eSJohn Snow            " but isn't suitable."
6114695a22eSJohn Snow        )
6124695a22eSJohn Snow    elif has_importlib:
6134695a22eSJohn Snow        lines.append(
6144695a22eSJohn Snow            f"Python package '{pkg_name}' was not found nor installed."
6154695a22eSJohn Snow        )
6164695a22eSJohn Snow    else:
6174695a22eSJohn Snow        lines.append(
6184695a22eSJohn Snow            f"Python package '{pkg_name}' is either not found or"
6194695a22eSJohn Snow            " not a suitable version."
6204695a22eSJohn Snow        )
6214695a22eSJohn Snow
6224695a22eSJohn Snow    if wheels_dir:
6234695a22eSJohn Snow        lines.append(
6244695a22eSJohn Snow            "No suitable version found in, or failed to install from"
6254695a22eSJohn Snow            f" '{wheels_dir}'."
6264695a22eSJohn Snow        )
6274695a22eSJohn Snow        bad = True
6284695a22eSJohn Snow
6294695a22eSJohn Snow    if online:
6304695a22eSJohn Snow        lines.append("A suitable version could not be obtained from PyPI.")
6314695a22eSJohn Snow        bad = True
6324695a22eSJohn Snow    else:
6334695a22eSJohn Snow        lines.append(
6344695a22eSJohn Snow            "mkvenv was configured to operate offline and did not check PyPI."
6354695a22eSJohn Snow        )
6364695a22eSJohn Snow
6374695a22eSJohn Snow    if prog and not pkg_version:
6384695a22eSJohn Snow        which = shutil.which(prog)
6394695a22eSJohn Snow        if which:
6404695a22eSJohn Snow            if sys.base_prefix in site.PREFIXES:
6414695a22eSJohn Snow                pypath = Path(sys.executable).resolve()
6424695a22eSJohn Snow                lines.append(
6434695a22eSJohn Snow                    f"'{prog}' was detected on your system at '{which}', "
6444695a22eSJohn Snow                    f"but the Python package '{pkg_name}' was not found by "
6454695a22eSJohn Snow                    f"this Python interpreter ('{pypath}'). "
6464695a22eSJohn Snow                    f"Typically this means that '{prog}' has been installed "
6474695a22eSJohn Snow                    "against a different Python interpreter on your system."
6484695a22eSJohn Snow                )
6494695a22eSJohn Snow            else:
6504695a22eSJohn Snow                lines.append(
6514695a22eSJohn Snow                    f"'{prog}' was detected on your system at '{which}', "
6524695a22eSJohn Snow                    "but the build is using an isolated virtual environment."
6534695a22eSJohn Snow                )
6544695a22eSJohn Snow            bad = True
6554695a22eSJohn Snow
6564695a22eSJohn Snow    lines = [f" • {line}" for line in lines]
6574695a22eSJohn Snow    if bad:
6584695a22eSJohn Snow        lines.insert(0, f"Could not provide build dependency '{dep_spec}':")
6594695a22eSJohn Snow    else:
6604695a22eSJohn Snow        lines.insert(0, f"'{dep_spec}' not found:")
6614695a22eSJohn Snow    return os.linesep.join(lines), bad
6624695a22eSJohn Snow
6634695a22eSJohn Snow
664c5538eedSJohn Snowdef pip_install(
665c5538eedSJohn Snow    args: Sequence[str],
666c5538eedSJohn Snow    online: bool = False,
667c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
668c5538eedSJohn Snow) -> None:
669c5538eedSJohn Snow    """
670c5538eedSJohn Snow    Use pip to install a package or package(s) as specified in @args.
671c5538eedSJohn Snow    """
672c5538eedSJohn Snow    loud = bool(
673c5538eedSJohn Snow        os.environ.get("DEBUG")
674c5538eedSJohn Snow        or os.environ.get("GITLAB_CI")
675c5538eedSJohn Snow        or os.environ.get("V")
676c5538eedSJohn Snow    )
677c5538eedSJohn Snow
678c5538eedSJohn Snow    full_args = [
679c5538eedSJohn Snow        sys.executable,
680c5538eedSJohn Snow        "-m",
681c5538eedSJohn Snow        "pip",
682c5538eedSJohn Snow        "install",
683c5538eedSJohn Snow        "--disable-pip-version-check",
684c5538eedSJohn Snow        "-v" if loud else "-q",
685c5538eedSJohn Snow    ]
686c5538eedSJohn Snow    if not online:
687c5538eedSJohn Snow        full_args += ["--no-index"]
688c5538eedSJohn Snow    if wheels_dir:
689c5538eedSJohn Snow        full_args += ["--find-links", f"file://{str(wheels_dir)}"]
690c5538eedSJohn Snow    full_args += list(args)
691c5538eedSJohn Snow    subprocess.run(
692c5538eedSJohn Snow        full_args,
693c5538eedSJohn Snow        check=True,
694c5538eedSJohn Snow    )
695c5538eedSJohn Snow
696c5538eedSJohn Snow
6974695a22eSJohn Snowdef _do_ensure(
698c5538eedSJohn Snow    dep_specs: Sequence[str],
699c5538eedSJohn Snow    online: bool = False,
700c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
701c5538eedSJohn Snow) -> None:
702c5538eedSJohn Snow    """
703c5538eedSJohn Snow    Use pip to ensure we have the package specified by @dep_specs.
704c5538eedSJohn Snow
705c5538eedSJohn Snow    If the package is already installed, do nothing. If online and
706c5538eedSJohn Snow    wheels_dir are both provided, prefer packages found in wheels_dir
707c5538eedSJohn Snow    first before connecting to PyPI.
708c5538eedSJohn Snow
709c5538eedSJohn Snow    :param dep_specs:
710c5538eedSJohn Snow        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
711c5538eedSJohn Snow    :param online: If True, fall back to PyPI.
712c5538eedSJohn Snow    :param wheels_dir: If specified, search this path for packages.
713c5538eedSJohn Snow    """
714c5538eedSJohn Snow    with warnings.catch_warnings():
715c5538eedSJohn Snow        warnings.filterwarnings(
716c5538eedSJohn Snow            "ignore", category=UserWarning, module="distlib"
717c5538eedSJohn Snow        )
718c5538eedSJohn Snow        dist_path = distlib.database.DistributionPath(include_egg=True)
719c5538eedSJohn Snow        absent = []
72092834894SJohn Snow        present = []
721c5538eedSJohn Snow        for spec in dep_specs:
722c5538eedSJohn Snow            matcher = distlib.version.LegacyMatcher(spec)
723c5538eedSJohn Snow            dist = dist_path.get_distribution(matcher.name)
724c5538eedSJohn Snow            if dist is None or not matcher.match(dist.version):
725c5538eedSJohn Snow                absent.append(spec)
726c5538eedSJohn Snow            else:
727c5538eedSJohn Snow                logger.info("found %s", dist)
72892834894SJohn Snow                present.append(matcher.name)
72992834894SJohn Snow
73092834894SJohn Snow    if present:
73192834894SJohn Snow        generate_console_scripts(present)
732c5538eedSJohn Snow
733c5538eedSJohn Snow    if absent:
734c5538eedSJohn Snow        # Some packages are missing or aren't a suitable version,
735c5538eedSJohn Snow        # install a suitable (possibly vendored) package.
736c5538eedSJohn Snow        print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr)
737c5538eedSJohn Snow        pip_install(args=absent, online=online, wheels_dir=wheels_dir)
738c5538eedSJohn Snow
739c5538eedSJohn Snow
7404695a22eSJohn Snowdef ensure(
7414695a22eSJohn Snow    dep_specs: Sequence[str],
7424695a22eSJohn Snow    online: bool = False,
7434695a22eSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
7444695a22eSJohn Snow    prog: Optional[str] = None,
7454695a22eSJohn Snow) -> None:
7464695a22eSJohn Snow    """
7474695a22eSJohn Snow    Use pip to ensure we have the package specified by @dep_specs.
7484695a22eSJohn Snow
7494695a22eSJohn Snow    If the package is already installed, do nothing. If online and
7504695a22eSJohn Snow    wheels_dir are both provided, prefer packages found in wheels_dir
7514695a22eSJohn Snow    first before connecting to PyPI.
7524695a22eSJohn Snow
7534695a22eSJohn Snow    :param dep_specs:
7544695a22eSJohn Snow        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
7554695a22eSJohn Snow    :param online: If True, fall back to PyPI.
7564695a22eSJohn Snow    :param wheels_dir: If specified, search this path for packages.
7574695a22eSJohn Snow    :param prog:
7584695a22eSJohn Snow        If specified, use this program name for error diagnostics that will
7594695a22eSJohn Snow        be presented to the user. e.g., 'sphinx-build' can be used as a
7604695a22eSJohn Snow        bellwether for the presence of 'sphinx'.
7614695a22eSJohn Snow    """
7624695a22eSJohn Snow    print(f"mkvenv: checking for {', '.join(dep_specs)}", file=sys.stderr)
76368ea6d17SJohn Snow
76468ea6d17SJohn Snow    if not HAVE_DISTLIB:
76568ea6d17SJohn Snow        raise Ouch("a usable distlib could not be found, please install it")
76668ea6d17SJohn Snow
7674695a22eSJohn Snow    try:
7684695a22eSJohn Snow        _do_ensure(dep_specs, online, wheels_dir)
7694695a22eSJohn Snow    except subprocess.CalledProcessError as exc:
7704695a22eSJohn Snow        # Well, that's not good.
7714695a22eSJohn Snow        msg, bad = diagnose(dep_specs[0], online, wheels_dir, prog)
7724695a22eSJohn Snow        if bad:
7734695a22eSJohn Snow            raise Ouch(msg) from exc
7744695a22eSJohn Snow        raise SystemExit(f"\n{msg}\n\n") from exc
7754695a22eSJohn Snow
7764695a22eSJohn Snow
777f1ad527fSJohn Snowdef post_venv_setup() -> None:
778f1ad527fSJohn Snow    """
779f1ad527fSJohn Snow    This is intended to be run *inside the venv* after it is created.
780f1ad527fSJohn Snow    """
781f1ad527fSJohn Snow    logger.debug("post_venv_setup()")
782c8049626SJohn Snow    # Test for a broken pip (Debian 10 or derivative?) and fix it if needed
783c8049626SJohn Snow    if not checkpip():
784c8049626SJohn Snow        # Finally, generate a 'pip' script so the venv is usable in a normal
785f1ad527fSJohn Snow        # way from the CLI. This only happens when we inherited pip from a
786f1ad527fSJohn Snow        # parent/system-site and haven't run ensurepip in some way.
787f1ad527fSJohn Snow        generate_console_scripts(["pip"])
788f1ad527fSJohn Snow
789f1ad527fSJohn Snow
790dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None:
791dd84028fSJohn Snow    subparser = subparsers.add_parser("create", help="create a venv")
792dd84028fSJohn Snow    subparser.add_argument(
793dd84028fSJohn Snow        "target",
794dd84028fSJohn Snow        type=str,
795dd84028fSJohn Snow        action="store",
796dd84028fSJohn Snow        help="Target directory to install virtual environment into.",
797dd84028fSJohn Snow    )
798dd84028fSJohn Snow
799dd84028fSJohn Snow
800f1ad527fSJohn Snowdef _add_post_init_subcommand(subparsers: Any) -> None:
801f1ad527fSJohn Snow    subparsers.add_parser("post_init", help="post-venv initialization")
802f1ad527fSJohn Snow
803f1ad527fSJohn Snow
804c5538eedSJohn Snowdef _add_ensure_subcommand(subparsers: Any) -> None:
805c5538eedSJohn Snow    subparser = subparsers.add_parser(
806c5538eedSJohn Snow        "ensure", help="Ensure that the specified package is installed."
807c5538eedSJohn Snow    )
808c5538eedSJohn Snow    subparser.add_argument(
809c5538eedSJohn Snow        "--online",
810c5538eedSJohn Snow        action="store_true",
811c5538eedSJohn Snow        help="Install packages from PyPI, if necessary.",
812c5538eedSJohn Snow    )
813c5538eedSJohn Snow    subparser.add_argument(
814c5538eedSJohn Snow        "--dir",
815c5538eedSJohn Snow        type=str,
816c5538eedSJohn Snow        action="store",
817c5538eedSJohn Snow        help="Path to vendored packages where we may install from.",
818c5538eedSJohn Snow    )
819c5538eedSJohn Snow    subparser.add_argument(
8204695a22eSJohn Snow        "--diagnose",
8214695a22eSJohn Snow        type=str,
8224695a22eSJohn Snow        action="store",
8234695a22eSJohn Snow        help=(
8244695a22eSJohn Snow            "Name of a shell utility to use for "
8254695a22eSJohn Snow            "diagnostics if this command fails."
8264695a22eSJohn Snow        ),
8274695a22eSJohn Snow    )
8284695a22eSJohn Snow    subparser.add_argument(
829c5538eedSJohn Snow        "dep_specs",
830c5538eedSJohn Snow        type=str,
831c5538eedSJohn Snow        action="store",
832c5538eedSJohn Snow        help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'",
833c5538eedSJohn Snow        nargs="+",
834c5538eedSJohn Snow    )
835c5538eedSJohn Snow
836c5538eedSJohn Snow
837dd84028fSJohn Snowdef main() -> int:
838dd84028fSJohn Snow    """CLI interface to make_qemu_venv. See module docstring."""
839dd84028fSJohn Snow    if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
840dd84028fSJohn Snow        # You're welcome.
841dd84028fSJohn Snow        logging.basicConfig(level=logging.DEBUG)
842c5538eedSJohn Snow    else:
843c5538eedSJohn Snow        if os.environ.get("V"):
844dd84028fSJohn Snow            logging.basicConfig(level=logging.INFO)
845dd84028fSJohn Snow
846c5538eedSJohn Snow        # These are incredibly noisy even for V=1
847c5538eedSJohn Snow        logging.getLogger("distlib.metadata").addFilter(lambda record: False)
848c5538eedSJohn Snow        logging.getLogger("distlib.database").addFilter(lambda record: False)
849c5538eedSJohn Snow
850dd84028fSJohn Snow    parser = argparse.ArgumentParser(
851dd84028fSJohn Snow        prog="mkvenv",
852dd84028fSJohn Snow        description="QEMU pyvenv bootstrapping utility",
853dd84028fSJohn Snow    )
854dd84028fSJohn Snow    subparsers = parser.add_subparsers(
855dd84028fSJohn Snow        title="Commands",
856dd84028fSJohn Snow        dest="command",
857*02312f1aSPaolo Bonzini        required=True,
858dd84028fSJohn Snow        metavar="command",
859dd84028fSJohn Snow        help="Description",
860dd84028fSJohn Snow    )
861dd84028fSJohn Snow
862dd84028fSJohn Snow    _add_create_subcommand(subparsers)
863f1ad527fSJohn Snow    _add_post_init_subcommand(subparsers)
864c5538eedSJohn Snow    _add_ensure_subcommand(subparsers)
865dd84028fSJohn Snow
866dd84028fSJohn Snow    args = parser.parse_args()
867dd84028fSJohn Snow    try:
868dd84028fSJohn Snow        if args.command == "create":
869dd84028fSJohn Snow            make_venv(
870dd84028fSJohn Snow                args.target,
871dd84028fSJohn Snow                system_site_packages=True,
872dd84028fSJohn Snow                clear=True,
873dd84028fSJohn Snow            )
874f1ad527fSJohn Snow        if args.command == "post_init":
875f1ad527fSJohn Snow            post_venv_setup()
876c5538eedSJohn Snow        if args.command == "ensure":
877c5538eedSJohn Snow            ensure(
878c5538eedSJohn Snow                dep_specs=args.dep_specs,
879c5538eedSJohn Snow                online=args.online,
880c5538eedSJohn Snow                wheels_dir=args.dir,
8814695a22eSJohn Snow                prog=args.diagnose,
882c5538eedSJohn Snow            )
883dd84028fSJohn Snow        logger.debug("mkvenv.py %s: exiting", args.command)
884dd84028fSJohn Snow    except Ouch as exc:
885dd84028fSJohn Snow        print("\n*** Ouch! ***\n", file=sys.stderr)
886dd84028fSJohn Snow        print(str(exc), "\n\n", file=sys.stderr)
887dd84028fSJohn Snow        return 1
888dd84028fSJohn Snow    except SystemExit:
889dd84028fSJohn Snow        raise
890dd84028fSJohn Snow    except:  # pylint: disable=bare-except
891dd84028fSJohn Snow        logger.exception("mkvenv did not complete successfully:")
892dd84028fSJohn Snow        return 2
893dd84028fSJohn Snow    return 0
894dd84028fSJohn Snow
895dd84028fSJohn Snow
896dd84028fSJohn Snowif __name__ == "__main__":
897dd84028fSJohn Snow    sys.exit(main())
898