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