xref: /qemu/python/scripts/mkvenv.py (revision c5538eed12e2e427e3715b39cca46c00afcde78d)
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
14*c5538eedSJohn Snow    ensure    Ensure that the specified package is installed.
15dd84028fSJohn Snow
16dd84028fSJohn Snow--------------------------------------------------
17dd84028fSJohn Snow
18dd84028fSJohn Snowusage: mkvenv create [-h] target
19dd84028fSJohn Snow
20dd84028fSJohn Snowpositional arguments:
21dd84028fSJohn Snow  target      Target directory to install virtual environment into.
22dd84028fSJohn Snow
23dd84028fSJohn Snowoptions:
24dd84028fSJohn Snow  -h, --help  show this help message and exit
25dd84028fSJohn Snow
26*c5538eedSJohn Snow--------------------------------------------------
27*c5538eedSJohn Snow
28*c5538eedSJohn Snowusage: mkvenv ensure [-h] [--online] [--dir DIR] dep_spec...
29*c5538eedSJohn Snow
30*c5538eedSJohn Snowpositional arguments:
31*c5538eedSJohn Snow  dep_spec    PEP 508 Dependency specification, e.g. 'meson>=0.61.5'
32*c5538eedSJohn Snow
33*c5538eedSJohn Snowoptions:
34*c5538eedSJohn Snow  -h, --help  show this help message and exit
35*c5538eedSJohn Snow  --online    Install packages from PyPI, if necessary.
36*c5538eedSJohn Snow  --dir DIR   Path to vendored packages where we may install from.
37*c5538eedSJohn Snow
38dd84028fSJohn Snow"""
39dd84028fSJohn Snow
40dd84028fSJohn Snow# Copyright (C) 2022-2023 Red Hat, Inc.
41dd84028fSJohn Snow#
42dd84028fSJohn Snow# Authors:
43dd84028fSJohn Snow#  John Snow <jsnow@redhat.com>
44dd84028fSJohn Snow#  Paolo Bonzini <pbonzini@redhat.com>
45dd84028fSJohn Snow#
46dd84028fSJohn Snow# This work is licensed under the terms of the GNU GPL, version 2 or
47dd84028fSJohn Snow# later. See the COPYING file in the top-level directory.
48dd84028fSJohn Snow
49dd84028fSJohn Snowimport argparse
50a9dbde71SJohn Snowfrom importlib.util import find_spec
51dd84028fSJohn Snowimport logging
52dd84028fSJohn Snowimport os
53dd84028fSJohn Snowfrom pathlib import Path
54dee01b82SJohn Snowimport site
55dd84028fSJohn Snowimport subprocess
56dd84028fSJohn Snowimport sys
57dee01b82SJohn Snowimport sysconfig
58dd84028fSJohn Snowfrom types import SimpleNamespace
59*c5538eedSJohn Snowfrom typing import (
60*c5538eedSJohn Snow    Any,
61*c5538eedSJohn Snow    Optional,
62*c5538eedSJohn Snow    Sequence,
63*c5538eedSJohn Snow    Union,
64*c5538eedSJohn Snow)
65dd84028fSJohn Snowimport venv
66*c5538eedSJohn Snowimport warnings
67*c5538eedSJohn Snow
68*c5538eedSJohn Snowimport distlib.database
69*c5538eedSJohn Snowimport distlib.version
70dd84028fSJohn Snow
71dd84028fSJohn Snow
72dd84028fSJohn Snow# Do not add any mandatory dependencies from outside the stdlib:
73dd84028fSJohn Snow# This script *must* be usable standalone!
74dd84028fSJohn Snow
75dd84028fSJohn SnowDirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
76dd84028fSJohn Snowlogger = logging.getLogger("mkvenv")
77dd84028fSJohn Snow
78dd84028fSJohn Snow
79dee01b82SJohn Snowdef inside_a_venv() -> bool:
80dee01b82SJohn Snow    """Returns True if it is executed inside of a virtual environment."""
81dee01b82SJohn Snow    return sys.prefix != sys.base_prefix
82dee01b82SJohn Snow
83dee01b82SJohn Snow
84dd84028fSJohn Snowclass Ouch(RuntimeError):
85dd84028fSJohn Snow    """An Exception class we can't confuse with a builtin."""
86dd84028fSJohn Snow
87dd84028fSJohn Snow
88dd84028fSJohn Snowclass QemuEnvBuilder(venv.EnvBuilder):
89dd84028fSJohn Snow    """
90dd84028fSJohn Snow    An extension of venv.EnvBuilder for building QEMU's configure-time venv.
91dd84028fSJohn Snow
92dee01b82SJohn Snow    The primary difference is that it emulates a "nested" virtual
93dee01b82SJohn Snow    environment when invoked from inside of an existing virtual
94dee01b82SJohn Snow    environment by including packages from the parent.
95dd84028fSJohn Snow
96dd84028fSJohn Snow    Parameters for base class init:
97dd84028fSJohn Snow      - system_site_packages: bool = False
98dd84028fSJohn Snow      - clear: bool = False
99dd84028fSJohn Snow      - symlinks: bool = False
100dd84028fSJohn Snow      - upgrade: bool = False
101dd84028fSJohn Snow      - with_pip: bool = False
102dd84028fSJohn Snow      - prompt: Optional[str] = None
103dd84028fSJohn Snow      - upgrade_deps: bool = False             (Since 3.9)
104dd84028fSJohn Snow    """
105dd84028fSJohn Snow
106dd84028fSJohn Snow    def __init__(self, *args: Any, **kwargs: Any) -> None:
107dd84028fSJohn Snow        logger.debug("QemuEnvBuilder.__init__(...)")
108a9dbde71SJohn Snow
109dee01b82SJohn Snow        # For nested venv emulation:
110dee01b82SJohn Snow        self.use_parent_packages = False
111dee01b82SJohn Snow        if inside_a_venv():
112dee01b82SJohn Snow            # Include parent packages only if we're in a venv and
113dee01b82SJohn Snow            # system_site_packages was True.
114dee01b82SJohn Snow            self.use_parent_packages = kwargs.pop(
115dee01b82SJohn Snow                "system_site_packages", False
116dee01b82SJohn Snow            )
117dee01b82SJohn Snow            # Include system_site_packages only when the parent,
118dee01b82SJohn Snow            # The venv we are currently in, also does so.
119dee01b82SJohn Snow            kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES
120dee01b82SJohn Snow
121a9dbde71SJohn Snow        if kwargs.get("with_pip", False):
122a9dbde71SJohn Snow            check_ensurepip()
123a9dbde71SJohn Snow
124dd84028fSJohn Snow        super().__init__(*args, **kwargs)
125dd84028fSJohn Snow
126dd84028fSJohn Snow        # Make the context available post-creation:
127dd84028fSJohn Snow        self._context: Optional[SimpleNamespace] = None
128dd84028fSJohn Snow
129dee01b82SJohn Snow    def get_parent_libpath(self) -> Optional[str]:
130dee01b82SJohn Snow        """Return the libpath of the parent venv, if applicable."""
131dee01b82SJohn Snow        if self.use_parent_packages:
132dee01b82SJohn Snow            return sysconfig.get_path("purelib")
133dee01b82SJohn Snow        return None
134dee01b82SJohn Snow
135dee01b82SJohn Snow    @staticmethod
136dee01b82SJohn Snow    def compute_venv_libpath(context: SimpleNamespace) -> str:
137dee01b82SJohn Snow        """
138dee01b82SJohn Snow        Compatibility wrapper for context.lib_path for Python < 3.12
139dee01b82SJohn Snow        """
140dee01b82SJohn Snow        # Python 3.12+, not strictly necessary because it's documented
141dee01b82SJohn Snow        # to be the same as 3.10 code below:
142dee01b82SJohn Snow        if sys.version_info >= (3, 12):
143dee01b82SJohn Snow            return context.lib_path
144dee01b82SJohn Snow
145dee01b82SJohn Snow        # Python 3.10+
146dee01b82SJohn Snow        if "venv" in sysconfig.get_scheme_names():
147dee01b82SJohn Snow            lib_path = sysconfig.get_path(
148dee01b82SJohn Snow                "purelib", scheme="venv", vars={"base": context.env_dir}
149dee01b82SJohn Snow            )
150dee01b82SJohn Snow            assert lib_path is not None
151dee01b82SJohn Snow            return lib_path
152dee01b82SJohn Snow
153dee01b82SJohn Snow        # For Python <= 3.9 we need to hardcode this. Fortunately the
154dee01b82SJohn Snow        # code below was the same in Python 3.6-3.10, so there is only
155dee01b82SJohn Snow        # one case.
156dee01b82SJohn Snow        if sys.platform == "win32":
157dee01b82SJohn Snow            return os.path.join(context.env_dir, "Lib", "site-packages")
158dee01b82SJohn Snow        return os.path.join(
159dee01b82SJohn Snow            context.env_dir,
160dee01b82SJohn Snow            "lib",
161dee01b82SJohn Snow            "python%d.%d" % sys.version_info[:2],
162dee01b82SJohn Snow            "site-packages",
163dee01b82SJohn Snow        )
164dee01b82SJohn Snow
165dd84028fSJohn Snow    def ensure_directories(self, env_dir: DirType) -> SimpleNamespace:
166dd84028fSJohn Snow        logger.debug("ensure_directories(env_dir=%s)", env_dir)
167dd84028fSJohn Snow        self._context = super().ensure_directories(env_dir)
168dd84028fSJohn Snow        return self._context
169dd84028fSJohn Snow
170dee01b82SJohn Snow    def create(self, env_dir: DirType) -> None:
171dee01b82SJohn Snow        logger.debug("create(env_dir=%s)", env_dir)
172dee01b82SJohn Snow        super().create(env_dir)
173dee01b82SJohn Snow        assert self._context is not None
174dee01b82SJohn Snow        self.post_post_setup(self._context)
175dee01b82SJohn Snow
176dee01b82SJohn Snow    def post_post_setup(self, context: SimpleNamespace) -> None:
177dee01b82SJohn Snow        """
178dee01b82SJohn Snow        The final, final hook. Enter the venv and run commands inside of it.
179dee01b82SJohn Snow        """
180dee01b82SJohn Snow        if self.use_parent_packages:
181dee01b82SJohn Snow            # We're inside of a venv and we want to include the parent
182dee01b82SJohn Snow            # venv's packages.
183dee01b82SJohn Snow            parent_libpath = self.get_parent_libpath()
184dee01b82SJohn Snow            assert parent_libpath is not None
185dee01b82SJohn Snow            logger.debug("parent_libpath: %s", parent_libpath)
186dee01b82SJohn Snow
187dee01b82SJohn Snow            our_libpath = self.compute_venv_libpath(context)
188dee01b82SJohn Snow            logger.debug("our_libpath: %s", our_libpath)
189dee01b82SJohn Snow
190dee01b82SJohn Snow            pth_file = os.path.join(our_libpath, "nested.pth")
191dee01b82SJohn Snow            with open(pth_file, "w", encoding="UTF-8") as file:
192dee01b82SJohn Snow                file.write(parent_libpath + os.linesep)
193dee01b82SJohn Snow
194dd84028fSJohn Snow    def get_value(self, field: str) -> str:
195dd84028fSJohn Snow        """
196dd84028fSJohn Snow        Get a string value from the context namespace after a call to build.
197dd84028fSJohn Snow
198dd84028fSJohn Snow        For valid field names, see:
199dd84028fSJohn Snow        https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories
200dd84028fSJohn Snow        """
201dd84028fSJohn Snow        ret = getattr(self._context, field)
202dd84028fSJohn Snow        assert isinstance(ret, str)
203dd84028fSJohn Snow        return ret
204dd84028fSJohn Snow
205dd84028fSJohn Snow
206a9dbde71SJohn Snowdef check_ensurepip() -> None:
207a9dbde71SJohn Snow    """
208a9dbde71SJohn Snow    Check that we have ensurepip.
209a9dbde71SJohn Snow
210a9dbde71SJohn Snow    Raise a fatal exception with a helpful hint if it isn't available.
211a9dbde71SJohn Snow    """
212a9dbde71SJohn Snow    if not find_spec("ensurepip"):
213a9dbde71SJohn Snow        msg = (
214a9dbde71SJohn Snow            "Python's ensurepip module is not found.\n"
215a9dbde71SJohn Snow            "It's normally part of the Python standard library, "
216a9dbde71SJohn Snow            "maybe your distribution packages it separately?\n"
217a9dbde71SJohn Snow            "Either install ensurepip, or alleviate the need for it in the "
218a9dbde71SJohn Snow            "first place by installing pip and setuptools for "
219a9dbde71SJohn Snow            f"'{sys.executable}'.\n"
220a9dbde71SJohn Snow            "(Hint: Debian puts ensurepip in its python3-venv package.)"
221a9dbde71SJohn Snow        )
222a9dbde71SJohn Snow        raise Ouch(msg)
223a9dbde71SJohn Snow
224a9dbde71SJohn Snow    # ensurepip uses pyexpat, which can also go missing on us:
225a9dbde71SJohn Snow    if not find_spec("pyexpat"):
226a9dbde71SJohn Snow        msg = (
227a9dbde71SJohn Snow            "Python's pyexpat module is not found.\n"
228a9dbde71SJohn Snow            "It's normally part of the Python standard library, "
229a9dbde71SJohn Snow            "maybe your distribution packages it separately?\n"
230a9dbde71SJohn Snow            "Either install pyexpat, or alleviate the need for it in the "
231a9dbde71SJohn Snow            "first place by installing pip and setuptools for "
232a9dbde71SJohn Snow            f"'{sys.executable}'.\n\n"
233a9dbde71SJohn Snow            "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)"
234a9dbde71SJohn Snow        )
235a9dbde71SJohn Snow        raise Ouch(msg)
236a9dbde71SJohn Snow
237a9dbde71SJohn Snow
238dd84028fSJohn Snowdef make_venv(  # pylint: disable=too-many-arguments
239dd84028fSJohn Snow    env_dir: Union[str, Path],
240dd84028fSJohn Snow    system_site_packages: bool = False,
241dd84028fSJohn Snow    clear: bool = True,
242dd84028fSJohn Snow    symlinks: Optional[bool] = None,
243dd84028fSJohn Snow    with_pip: bool = True,
244dd84028fSJohn Snow) -> None:
245dd84028fSJohn Snow    """
246dd84028fSJohn Snow    Create a venv using `QemuEnvBuilder`.
247dd84028fSJohn Snow
248dd84028fSJohn Snow    This is analogous to the `venv.create` module-level convenience
249dd84028fSJohn Snow    function that is part of the Python stdblib, except it uses
250dd84028fSJohn Snow    `QemuEnvBuilder` instead.
251dd84028fSJohn Snow
252dd84028fSJohn Snow    :param env_dir: The directory to create/install to.
253dd84028fSJohn Snow    :param system_site_packages:
254dd84028fSJohn Snow        Allow inheriting packages from the system installation.
255dd84028fSJohn Snow    :param clear: When True, fully remove any prior venv and files.
256dd84028fSJohn Snow    :param symlinks:
257dd84028fSJohn Snow        Whether to use symlinks to the target interpreter or not. If
258dd84028fSJohn Snow        left unspecified, it will use symlinks except on Windows to
259dd84028fSJohn Snow        match behavior with the "venv" CLI tool.
260dd84028fSJohn Snow    :param with_pip:
261dd84028fSJohn Snow        Whether to install "pip" binaries or not.
262dd84028fSJohn Snow    """
263dd84028fSJohn Snow    logger.debug(
264dd84028fSJohn Snow        "%s: make_venv(env_dir=%s, system_site_packages=%s, "
265dd84028fSJohn Snow        "clear=%s, symlinks=%s, with_pip=%s)",
266dd84028fSJohn Snow        __file__,
267dd84028fSJohn Snow        str(env_dir),
268dd84028fSJohn Snow        system_site_packages,
269dd84028fSJohn Snow        clear,
270dd84028fSJohn Snow        symlinks,
271dd84028fSJohn Snow        with_pip,
272dd84028fSJohn Snow    )
273dd84028fSJohn Snow
274dd84028fSJohn Snow    if symlinks is None:
275dd84028fSJohn Snow        # Default behavior of standard venv CLI
276dd84028fSJohn Snow        symlinks = os.name != "nt"
277dd84028fSJohn Snow
278dd84028fSJohn Snow    builder = QemuEnvBuilder(
279dd84028fSJohn Snow        system_site_packages=system_site_packages,
280dd84028fSJohn Snow        clear=clear,
281dd84028fSJohn Snow        symlinks=symlinks,
282dd84028fSJohn Snow        with_pip=with_pip,
283dd84028fSJohn Snow    )
284dd84028fSJohn Snow
285dd84028fSJohn Snow    style = "non-isolated" if builder.system_site_packages else "isolated"
286dee01b82SJohn Snow    nested = ""
287dee01b82SJohn Snow    if builder.use_parent_packages:
288dee01b82SJohn Snow        nested = f"(with packages from '{builder.get_parent_libpath()}') "
289dd84028fSJohn Snow    print(
290dd84028fSJohn Snow        f"mkvenv: Creating {style} virtual environment"
291dee01b82SJohn Snow        f" {nested}at '{str(env_dir)}'",
292dd84028fSJohn Snow        file=sys.stderr,
293dd84028fSJohn Snow    )
294dd84028fSJohn Snow
295dd84028fSJohn Snow    try:
296dd84028fSJohn Snow        logger.debug("Invoking builder.create()")
297dd84028fSJohn Snow        try:
298dd84028fSJohn Snow            builder.create(str(env_dir))
299dd84028fSJohn Snow        except SystemExit as exc:
300dd84028fSJohn Snow            # Some versions of the venv module raise SystemExit; *nasty*!
301dd84028fSJohn Snow            # We want the exception that prompted it. It might be a subprocess
302dd84028fSJohn Snow            # error that has output we *really* want to see.
303dd84028fSJohn Snow            logger.debug("Intercepted SystemExit from EnvBuilder.create()")
304dd84028fSJohn Snow            raise exc.__cause__ or exc.__context__ or exc
305dd84028fSJohn Snow        logger.debug("builder.create() finished")
306dd84028fSJohn Snow    except subprocess.CalledProcessError as exc:
307dd84028fSJohn Snow        logger.error("mkvenv subprocess failed:")
308dd84028fSJohn Snow        logger.error("cmd: %s", exc.cmd)
309dd84028fSJohn Snow        logger.error("returncode: %d", exc.returncode)
310dd84028fSJohn Snow
311dd84028fSJohn Snow        def _stringify(data: Union[str, bytes]) -> str:
312dd84028fSJohn Snow            if isinstance(data, bytes):
313dd84028fSJohn Snow                return data.decode()
314dd84028fSJohn Snow            return data
315dd84028fSJohn Snow
316dd84028fSJohn Snow        lines = []
317dd84028fSJohn Snow        if exc.stdout:
318dd84028fSJohn Snow            lines.append("========== stdout ==========")
319dd84028fSJohn Snow            lines.append(_stringify(exc.stdout))
320dd84028fSJohn Snow            lines.append("============================")
321dd84028fSJohn Snow        if exc.stderr:
322dd84028fSJohn Snow            lines.append("========== stderr ==========")
323dd84028fSJohn Snow            lines.append(_stringify(exc.stderr))
324dd84028fSJohn Snow            lines.append("============================")
325dd84028fSJohn Snow        if lines:
326dd84028fSJohn Snow            logger.error(os.linesep.join(lines))
327dd84028fSJohn Snow
328dd84028fSJohn Snow        raise Ouch("VENV creation subprocess failed.") from exc
329dd84028fSJohn Snow
330dd84028fSJohn Snow    # print the python executable to stdout for configure.
331dd84028fSJohn Snow    print(builder.get_value("env_exe"))
332dd84028fSJohn Snow
333dd84028fSJohn Snow
334*c5538eedSJohn Snowdef pip_install(
335*c5538eedSJohn Snow    args: Sequence[str],
336*c5538eedSJohn Snow    online: bool = False,
337*c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
338*c5538eedSJohn Snow) -> None:
339*c5538eedSJohn Snow    """
340*c5538eedSJohn Snow    Use pip to install a package or package(s) as specified in @args.
341*c5538eedSJohn Snow    """
342*c5538eedSJohn Snow    loud = bool(
343*c5538eedSJohn Snow        os.environ.get("DEBUG")
344*c5538eedSJohn Snow        or os.environ.get("GITLAB_CI")
345*c5538eedSJohn Snow        or os.environ.get("V")
346*c5538eedSJohn Snow    )
347*c5538eedSJohn Snow
348*c5538eedSJohn Snow    full_args = [
349*c5538eedSJohn Snow        sys.executable,
350*c5538eedSJohn Snow        "-m",
351*c5538eedSJohn Snow        "pip",
352*c5538eedSJohn Snow        "install",
353*c5538eedSJohn Snow        "--disable-pip-version-check",
354*c5538eedSJohn Snow        "-v" if loud else "-q",
355*c5538eedSJohn Snow    ]
356*c5538eedSJohn Snow    if not online:
357*c5538eedSJohn Snow        full_args += ["--no-index"]
358*c5538eedSJohn Snow    if wheels_dir:
359*c5538eedSJohn Snow        full_args += ["--find-links", f"file://{str(wheels_dir)}"]
360*c5538eedSJohn Snow    full_args += list(args)
361*c5538eedSJohn Snow    subprocess.run(
362*c5538eedSJohn Snow        full_args,
363*c5538eedSJohn Snow        check=True,
364*c5538eedSJohn Snow    )
365*c5538eedSJohn Snow
366*c5538eedSJohn Snow
367*c5538eedSJohn Snowdef ensure(
368*c5538eedSJohn Snow    dep_specs: Sequence[str],
369*c5538eedSJohn Snow    online: bool = False,
370*c5538eedSJohn Snow    wheels_dir: Optional[Union[str, Path]] = None,
371*c5538eedSJohn Snow) -> None:
372*c5538eedSJohn Snow    """
373*c5538eedSJohn Snow    Use pip to ensure we have the package specified by @dep_specs.
374*c5538eedSJohn Snow
375*c5538eedSJohn Snow    If the package is already installed, do nothing. If online and
376*c5538eedSJohn Snow    wheels_dir are both provided, prefer packages found in wheels_dir
377*c5538eedSJohn Snow    first before connecting to PyPI.
378*c5538eedSJohn Snow
379*c5538eedSJohn Snow    :param dep_specs:
380*c5538eedSJohn Snow        PEP 508 dependency specifications. e.g. ['meson>=0.61.5'].
381*c5538eedSJohn Snow    :param online: If True, fall back to PyPI.
382*c5538eedSJohn Snow    :param wheels_dir: If specified, search this path for packages.
383*c5538eedSJohn Snow    """
384*c5538eedSJohn Snow    with warnings.catch_warnings():
385*c5538eedSJohn Snow        warnings.filterwarnings(
386*c5538eedSJohn Snow            "ignore", category=UserWarning, module="distlib"
387*c5538eedSJohn Snow        )
388*c5538eedSJohn Snow        dist_path = distlib.database.DistributionPath(include_egg=True)
389*c5538eedSJohn Snow        absent = []
390*c5538eedSJohn Snow        for spec in dep_specs:
391*c5538eedSJohn Snow            matcher = distlib.version.LegacyMatcher(spec)
392*c5538eedSJohn Snow            dist = dist_path.get_distribution(matcher.name)
393*c5538eedSJohn Snow            if dist is None or not matcher.match(dist.version):
394*c5538eedSJohn Snow                absent.append(spec)
395*c5538eedSJohn Snow            else:
396*c5538eedSJohn Snow                logger.info("found %s", dist)
397*c5538eedSJohn Snow
398*c5538eedSJohn Snow    if absent:
399*c5538eedSJohn Snow        # Some packages are missing or aren't a suitable version,
400*c5538eedSJohn Snow        # install a suitable (possibly vendored) package.
401*c5538eedSJohn Snow        print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr)
402*c5538eedSJohn Snow        pip_install(args=absent, online=online, wheels_dir=wheels_dir)
403*c5538eedSJohn Snow
404*c5538eedSJohn Snow
405dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None:
406dd84028fSJohn Snow    subparser = subparsers.add_parser("create", help="create a venv")
407dd84028fSJohn Snow    subparser.add_argument(
408dd84028fSJohn Snow        "target",
409dd84028fSJohn Snow        type=str,
410dd84028fSJohn Snow        action="store",
411dd84028fSJohn Snow        help="Target directory to install virtual environment into.",
412dd84028fSJohn Snow    )
413dd84028fSJohn Snow
414dd84028fSJohn Snow
415*c5538eedSJohn Snowdef _add_ensure_subcommand(subparsers: Any) -> None:
416*c5538eedSJohn Snow    subparser = subparsers.add_parser(
417*c5538eedSJohn Snow        "ensure", help="Ensure that the specified package is installed."
418*c5538eedSJohn Snow    )
419*c5538eedSJohn Snow    subparser.add_argument(
420*c5538eedSJohn Snow        "--online",
421*c5538eedSJohn Snow        action="store_true",
422*c5538eedSJohn Snow        help="Install packages from PyPI, if necessary.",
423*c5538eedSJohn Snow    )
424*c5538eedSJohn Snow    subparser.add_argument(
425*c5538eedSJohn Snow        "--dir",
426*c5538eedSJohn Snow        type=str,
427*c5538eedSJohn Snow        action="store",
428*c5538eedSJohn Snow        help="Path to vendored packages where we may install from.",
429*c5538eedSJohn Snow    )
430*c5538eedSJohn Snow    subparser.add_argument(
431*c5538eedSJohn Snow        "dep_specs",
432*c5538eedSJohn Snow        type=str,
433*c5538eedSJohn Snow        action="store",
434*c5538eedSJohn Snow        help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'",
435*c5538eedSJohn Snow        nargs="+",
436*c5538eedSJohn Snow    )
437*c5538eedSJohn Snow
438*c5538eedSJohn Snow
439dd84028fSJohn Snowdef main() -> int:
440dd84028fSJohn Snow    """CLI interface to make_qemu_venv. See module docstring."""
441dd84028fSJohn Snow    if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
442dd84028fSJohn Snow        # You're welcome.
443dd84028fSJohn Snow        logging.basicConfig(level=logging.DEBUG)
444*c5538eedSJohn Snow    else:
445*c5538eedSJohn Snow        if os.environ.get("V"):
446dd84028fSJohn Snow            logging.basicConfig(level=logging.INFO)
447dd84028fSJohn Snow
448*c5538eedSJohn Snow        # These are incredibly noisy even for V=1
449*c5538eedSJohn Snow        logging.getLogger("distlib.metadata").addFilter(lambda record: False)
450*c5538eedSJohn Snow        logging.getLogger("distlib.database").addFilter(lambda record: False)
451*c5538eedSJohn Snow
452dd84028fSJohn Snow    parser = argparse.ArgumentParser(
453dd84028fSJohn Snow        prog="mkvenv",
454dd84028fSJohn Snow        description="QEMU pyvenv bootstrapping utility",
455dd84028fSJohn Snow    )
456dd84028fSJohn Snow    subparsers = parser.add_subparsers(
457dd84028fSJohn Snow        title="Commands",
458dd84028fSJohn Snow        dest="command",
459dd84028fSJohn Snow        metavar="command",
460dd84028fSJohn Snow        help="Description",
461dd84028fSJohn Snow    )
462dd84028fSJohn Snow
463dd84028fSJohn Snow    _add_create_subcommand(subparsers)
464*c5538eedSJohn Snow    _add_ensure_subcommand(subparsers)
465dd84028fSJohn Snow
466dd84028fSJohn Snow    args = parser.parse_args()
467dd84028fSJohn Snow    try:
468dd84028fSJohn Snow        if args.command == "create":
469dd84028fSJohn Snow            make_venv(
470dd84028fSJohn Snow                args.target,
471dd84028fSJohn Snow                system_site_packages=True,
472dd84028fSJohn Snow                clear=True,
473dd84028fSJohn Snow            )
474*c5538eedSJohn Snow        if args.command == "ensure":
475*c5538eedSJohn Snow            ensure(
476*c5538eedSJohn Snow                dep_specs=args.dep_specs,
477*c5538eedSJohn Snow                online=args.online,
478*c5538eedSJohn Snow                wheels_dir=args.dir,
479*c5538eedSJohn Snow            )
480dd84028fSJohn Snow        logger.debug("mkvenv.py %s: exiting", args.command)
481dd84028fSJohn Snow    except Ouch as exc:
482dd84028fSJohn Snow        print("\n*** Ouch! ***\n", file=sys.stderr)
483dd84028fSJohn Snow        print(str(exc), "\n\n", file=sys.stderr)
484dd84028fSJohn Snow        return 1
485dd84028fSJohn Snow    except SystemExit:
486dd84028fSJohn Snow        raise
487dd84028fSJohn Snow    except:  # pylint: disable=bare-except
488dd84028fSJohn Snow        logger.exception("mkvenv did not complete successfully:")
489dd84028fSJohn Snow        return 2
490dd84028fSJohn Snow    return 0
491dd84028fSJohn Snow
492dd84028fSJohn Snow
493dd84028fSJohn Snowif __name__ == "__main__":
494dd84028fSJohn Snow    sys.exit(main())
495