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