xref: /openbmc/qemu/python/scripts/mkvenv.py (revision dd84028f)
1"""
2mkvenv - QEMU pyvenv bootstrapping utility
3
4usage: mkvenv [-h] command ...
5
6QEMU pyvenv bootstrapping utility
7
8options:
9  -h, --help  show this help message and exit
10
11Commands:
12  command     Description
13    create    create a venv
14
15--------------------------------------------------
16
17usage: mkvenv create [-h] target
18
19positional arguments:
20  target      Target directory to install virtual environment into.
21
22options:
23  -h, --help  show this help message and exit
24
25"""
26
27# Copyright (C) 2022-2023 Red Hat, Inc.
28#
29# Authors:
30#  John Snow <jsnow@redhat.com>
31#  Paolo Bonzini <pbonzini@redhat.com>
32#
33# This work is licensed under the terms of the GNU GPL, version 2 or
34# later. See the COPYING file in the top-level directory.
35
36import argparse
37import logging
38import os
39from pathlib import Path
40import subprocess
41import sys
42from types import SimpleNamespace
43from typing import Any, Optional, Union
44import venv
45
46
47# Do not add any mandatory dependencies from outside the stdlib:
48# This script *must* be usable standalone!
49
50DirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
51logger = logging.getLogger("mkvenv")
52
53
54class Ouch(RuntimeError):
55    """An Exception class we can't confuse with a builtin."""
56
57
58class QemuEnvBuilder(venv.EnvBuilder):
59    """
60    An extension of venv.EnvBuilder for building QEMU's configure-time venv.
61
62    As of this commit, it does not yet do anything particularly
63    different than the standard venv-creation utility. The next several
64    commits will gradually change that in small commits that highlight
65    each feature individually.
66
67    Parameters for base class init:
68      - system_site_packages: bool = False
69      - clear: bool = False
70      - symlinks: bool = False
71      - upgrade: bool = False
72      - with_pip: bool = False
73      - prompt: Optional[str] = None
74      - upgrade_deps: bool = False             (Since 3.9)
75    """
76
77    def __init__(self, *args: Any, **kwargs: Any) -> None:
78        logger.debug("QemuEnvBuilder.__init__(...)")
79        super().__init__(*args, **kwargs)
80
81        # Make the context available post-creation:
82        self._context: Optional[SimpleNamespace] = None
83
84    def ensure_directories(self, env_dir: DirType) -> SimpleNamespace:
85        logger.debug("ensure_directories(env_dir=%s)", env_dir)
86        self._context = super().ensure_directories(env_dir)
87        return self._context
88
89    def get_value(self, field: str) -> str:
90        """
91        Get a string value from the context namespace after a call to build.
92
93        For valid field names, see:
94        https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories
95        """
96        ret = getattr(self._context, field)
97        assert isinstance(ret, str)
98        return ret
99
100
101def make_venv(  # pylint: disable=too-many-arguments
102    env_dir: Union[str, Path],
103    system_site_packages: bool = False,
104    clear: bool = True,
105    symlinks: Optional[bool] = None,
106    with_pip: bool = True,
107) -> None:
108    """
109    Create a venv using `QemuEnvBuilder`.
110
111    This is analogous to the `venv.create` module-level convenience
112    function that is part of the Python stdblib, except it uses
113    `QemuEnvBuilder` instead.
114
115    :param env_dir: The directory to create/install to.
116    :param system_site_packages:
117        Allow inheriting packages from the system installation.
118    :param clear: When True, fully remove any prior venv and files.
119    :param symlinks:
120        Whether to use symlinks to the target interpreter or not. If
121        left unspecified, it will use symlinks except on Windows to
122        match behavior with the "venv" CLI tool.
123    :param with_pip:
124        Whether to install "pip" binaries or not.
125    """
126    logger.debug(
127        "%s: make_venv(env_dir=%s, system_site_packages=%s, "
128        "clear=%s, symlinks=%s, with_pip=%s)",
129        __file__,
130        str(env_dir),
131        system_site_packages,
132        clear,
133        symlinks,
134        with_pip,
135    )
136
137    if symlinks is None:
138        # Default behavior of standard venv CLI
139        symlinks = os.name != "nt"
140
141    builder = QemuEnvBuilder(
142        system_site_packages=system_site_packages,
143        clear=clear,
144        symlinks=symlinks,
145        with_pip=with_pip,
146    )
147
148    style = "non-isolated" if builder.system_site_packages else "isolated"
149    print(
150        f"mkvenv: Creating {style} virtual environment"
151        f" at '{str(env_dir)}'",
152        file=sys.stderr,
153    )
154
155    try:
156        logger.debug("Invoking builder.create()")
157        try:
158            builder.create(str(env_dir))
159        except SystemExit as exc:
160            # Some versions of the venv module raise SystemExit; *nasty*!
161            # We want the exception that prompted it. It might be a subprocess
162            # error that has output we *really* want to see.
163            logger.debug("Intercepted SystemExit from EnvBuilder.create()")
164            raise exc.__cause__ or exc.__context__ or exc
165        logger.debug("builder.create() finished")
166    except subprocess.CalledProcessError as exc:
167        logger.error("mkvenv subprocess failed:")
168        logger.error("cmd: %s", exc.cmd)
169        logger.error("returncode: %d", exc.returncode)
170
171        def _stringify(data: Union[str, bytes]) -> str:
172            if isinstance(data, bytes):
173                return data.decode()
174            return data
175
176        lines = []
177        if exc.stdout:
178            lines.append("========== stdout ==========")
179            lines.append(_stringify(exc.stdout))
180            lines.append("============================")
181        if exc.stderr:
182            lines.append("========== stderr ==========")
183            lines.append(_stringify(exc.stderr))
184            lines.append("============================")
185        if lines:
186            logger.error(os.linesep.join(lines))
187
188        raise Ouch("VENV creation subprocess failed.") from exc
189
190    # print the python executable to stdout for configure.
191    print(builder.get_value("env_exe"))
192
193
194def _add_create_subcommand(subparsers: Any) -> None:
195    subparser = subparsers.add_parser("create", help="create a venv")
196    subparser.add_argument(
197        "target",
198        type=str,
199        action="store",
200        help="Target directory to install virtual environment into.",
201    )
202
203
204def main() -> int:
205    """CLI interface to make_qemu_venv. See module docstring."""
206    if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
207        # You're welcome.
208        logging.basicConfig(level=logging.DEBUG)
209    elif os.environ.get("V"):
210        logging.basicConfig(level=logging.INFO)
211
212    parser = argparse.ArgumentParser(
213        prog="mkvenv",
214        description="QEMU pyvenv bootstrapping utility",
215    )
216    subparsers = parser.add_subparsers(
217        title="Commands",
218        dest="command",
219        metavar="command",
220        help="Description",
221    )
222
223    _add_create_subcommand(subparsers)
224
225    args = parser.parse_args()
226    try:
227        if args.command == "create":
228            make_venv(
229                args.target,
230                system_site_packages=True,
231                clear=True,
232            )
233        logger.debug("mkvenv.py %s: exiting", args.command)
234    except Ouch as exc:
235        print("\n*** Ouch! ***\n", file=sys.stderr)
236        print(str(exc), "\n\n", file=sys.stderr)
237        return 1
238    except SystemExit:
239        raise
240    except:  # pylint: disable=bare-except
241        logger.exception("mkvenv did not complete successfully:")
242        return 2
243    return 0
244
245
246if __name__ == "__main__":
247    sys.exit(main())
248