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*f1ad527fSJohn Snow post_init 15*f1ad527fSJohn 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 30*f1ad527fSJohn Snowusage: mkvenv post_init [-h] 31*f1ad527fSJohn Snow 32*f1ad527fSJohn Snowoptions: 33*f1ad527fSJohn Snow -h, --help show this help message and exit 34*f1ad527fSJohn Snow 35*f1ad527fSJohn Snow-------------------------------------------------- 36*f1ad527fSJohn 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 123*f1ad527fSJohn Snow environment by including packages from the parent. Also, 124*f1ad527fSJohn Snow "ensurepip" is replaced if possible with just recreating pip's 125*f1ad527fSJohn 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 152*f1ad527fSJohn Snow # ensurepip is slow: venv creation can be very fast for cases where 153*f1ad527fSJohn Snow # we allow the use of system_site_packages. Therefore, ensurepip is 154*f1ad527fSJohn Snow # replaced with our own script generation once the virtual environment 155*f1ad527fSJohn Snow # is setup. 156*f1ad527fSJohn Snow self.want_pip = kwargs.get("with_pip", False) 157*f1ad527fSJohn Snow if self.want_pip: 158*f1ad527fSJohn Snow if ( 159*f1ad527fSJohn Snow kwargs.get("system_site_packages", False) 160*f1ad527fSJohn Snow and not need_ensurepip() 161*f1ad527fSJohn Snow ): 162*f1ad527fSJohn Snow kwargs["with_pip"] = False 163*f1ad527fSJohn Snow else: 164a9dbde71SJohn Snow check_ensurepip() 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 236*f1ad527fSJohn Snow if self.want_pip: 237*f1ad527fSJohn Snow args = [ 238*f1ad527fSJohn Snow context.env_exe, 239*f1ad527fSJohn Snow __file__, 240*f1ad527fSJohn Snow "post_init", 241*f1ad527fSJohn Snow ] 242*f1ad527fSJohn Snow subprocess.run(args, check=True) 243*f1ad527fSJohn 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 256*f1ad527fSJohn Snowdef need_ensurepip() -> bool: 257*f1ad527fSJohn Snow """ 258*f1ad527fSJohn Snow Tests for the presence of setuptools and pip. 259*f1ad527fSJohn Snow 260*f1ad527fSJohn Snow :return: `True` if we do not detect both packages. 261*f1ad527fSJohn Snow """ 262*f1ad527fSJohn Snow # Don't try to actually import them, it's fraught with danger: 263*f1ad527fSJohn Snow # https://github.com/pypa/setuptools/issues/2993 264*f1ad527fSJohn Snow if find_spec("setuptools") and find_spec("pip"): 265*f1ad527fSJohn Snow return False 266*f1ad527fSJohn Snow return True 267*f1ad527fSJohn Snow 268*f1ad527fSJohn Snow 269a9dbde71SJohn Snowdef check_ensurepip() -> 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" 280a9dbde71SJohn Snow "Either install ensurepip, or alleviate the need for it in the " 281a9dbde71SJohn Snow "first place by installing pip and setuptools for " 282a9dbde71SJohn Snow f"'{sys.executable}'.\n" 283a9dbde71SJohn Snow "(Hint: Debian puts ensurepip in its python3-venv package.)" 284a9dbde71SJohn Snow ) 285a9dbde71SJohn Snow raise Ouch(msg) 286a9dbde71SJohn Snow 287a9dbde71SJohn Snow # ensurepip uses pyexpat, which can also go missing on us: 288a9dbde71SJohn Snow if not find_spec("pyexpat"): 289a9dbde71SJohn Snow msg = ( 290a9dbde71SJohn Snow "Python's pyexpat module is not found.\n" 291a9dbde71SJohn Snow "It's normally part of the Python standard library, " 292a9dbde71SJohn Snow "maybe your distribution packages it separately?\n" 293a9dbde71SJohn Snow "Either install pyexpat, or alleviate the need for it in the " 294a9dbde71SJohn Snow "first place by installing pip and setuptools for " 295a9dbde71SJohn Snow f"'{sys.executable}'.\n\n" 296a9dbde71SJohn Snow "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)" 297a9dbde71SJohn Snow ) 298a9dbde71SJohn Snow raise Ouch(msg) 299a9dbde71SJohn Snow 300a9dbde71SJohn Snow 301dd84028fSJohn Snowdef make_venv( # pylint: disable=too-many-arguments 302dd84028fSJohn Snow env_dir: Union[str, Path], 303dd84028fSJohn Snow system_site_packages: bool = False, 304dd84028fSJohn Snow clear: bool = True, 305dd84028fSJohn Snow symlinks: Optional[bool] = None, 306dd84028fSJohn Snow with_pip: bool = True, 307dd84028fSJohn Snow) -> None: 308dd84028fSJohn Snow """ 309dd84028fSJohn Snow Create a venv using `QemuEnvBuilder`. 310dd84028fSJohn Snow 311dd84028fSJohn Snow This is analogous to the `venv.create` module-level convenience 312dd84028fSJohn Snow function that is part of the Python stdblib, except it uses 313dd84028fSJohn Snow `QemuEnvBuilder` instead. 314dd84028fSJohn Snow 315dd84028fSJohn Snow :param env_dir: The directory to create/install to. 316dd84028fSJohn Snow :param system_site_packages: 317dd84028fSJohn Snow Allow inheriting packages from the system installation. 318dd84028fSJohn Snow :param clear: When True, fully remove any prior venv and files. 319dd84028fSJohn Snow :param symlinks: 320dd84028fSJohn Snow Whether to use symlinks to the target interpreter or not. If 321dd84028fSJohn Snow left unspecified, it will use symlinks except on Windows to 322dd84028fSJohn Snow match behavior with the "venv" CLI tool. 323dd84028fSJohn Snow :param with_pip: 324dd84028fSJohn Snow Whether to install "pip" binaries or not. 325dd84028fSJohn Snow """ 326dd84028fSJohn Snow logger.debug( 327dd84028fSJohn Snow "%s: make_venv(env_dir=%s, system_site_packages=%s, " 328dd84028fSJohn Snow "clear=%s, symlinks=%s, with_pip=%s)", 329dd84028fSJohn Snow __file__, 330dd84028fSJohn Snow str(env_dir), 331dd84028fSJohn Snow system_site_packages, 332dd84028fSJohn Snow clear, 333dd84028fSJohn Snow symlinks, 334dd84028fSJohn Snow with_pip, 335dd84028fSJohn Snow ) 336dd84028fSJohn Snow 337dd84028fSJohn Snow if symlinks is None: 338dd84028fSJohn Snow # Default behavior of standard venv CLI 339dd84028fSJohn Snow symlinks = os.name != "nt" 340dd84028fSJohn Snow 341dd84028fSJohn Snow builder = QemuEnvBuilder( 342dd84028fSJohn Snow system_site_packages=system_site_packages, 343dd84028fSJohn Snow clear=clear, 344dd84028fSJohn Snow symlinks=symlinks, 345dd84028fSJohn Snow with_pip=with_pip, 346dd84028fSJohn Snow ) 347dd84028fSJohn Snow 348dd84028fSJohn Snow style = "non-isolated" if builder.system_site_packages else "isolated" 349dee01b82SJohn Snow nested = "" 350dee01b82SJohn Snow if builder.use_parent_packages: 351dee01b82SJohn Snow nested = f"(with packages from '{builder.get_parent_libpath()}') " 352dd84028fSJohn Snow print( 353dd84028fSJohn Snow f"mkvenv: Creating {style} virtual environment" 354dee01b82SJohn Snow f" {nested}at '{str(env_dir)}'", 355dd84028fSJohn Snow file=sys.stderr, 356dd84028fSJohn Snow ) 357dd84028fSJohn Snow 358dd84028fSJohn Snow try: 359dd84028fSJohn Snow logger.debug("Invoking builder.create()") 360dd84028fSJohn Snow try: 361dd84028fSJohn Snow builder.create(str(env_dir)) 362dd84028fSJohn Snow except SystemExit as exc: 363dd84028fSJohn Snow # Some versions of the venv module raise SystemExit; *nasty*! 364dd84028fSJohn Snow # We want the exception that prompted it. It might be a subprocess 365dd84028fSJohn Snow # error that has output we *really* want to see. 366dd84028fSJohn Snow logger.debug("Intercepted SystemExit from EnvBuilder.create()") 367dd84028fSJohn Snow raise exc.__cause__ or exc.__context__ or exc 368dd84028fSJohn Snow logger.debug("builder.create() finished") 369dd84028fSJohn Snow except subprocess.CalledProcessError as exc: 370dd84028fSJohn Snow logger.error("mkvenv subprocess failed:") 371dd84028fSJohn Snow logger.error("cmd: %s", exc.cmd) 372dd84028fSJohn Snow logger.error("returncode: %d", exc.returncode) 373dd84028fSJohn Snow 374dd84028fSJohn Snow def _stringify(data: Union[str, bytes]) -> str: 375dd84028fSJohn Snow if isinstance(data, bytes): 376dd84028fSJohn Snow return data.decode() 377dd84028fSJohn Snow return data 378dd84028fSJohn Snow 379dd84028fSJohn Snow lines = [] 380dd84028fSJohn Snow if exc.stdout: 381dd84028fSJohn Snow lines.append("========== stdout ==========") 382dd84028fSJohn Snow lines.append(_stringify(exc.stdout)) 383dd84028fSJohn Snow lines.append("============================") 384dd84028fSJohn Snow if exc.stderr: 385dd84028fSJohn Snow lines.append("========== stderr ==========") 386dd84028fSJohn Snow lines.append(_stringify(exc.stderr)) 387dd84028fSJohn Snow lines.append("============================") 388dd84028fSJohn Snow if lines: 389dd84028fSJohn Snow logger.error(os.linesep.join(lines)) 390dd84028fSJohn Snow 391dd84028fSJohn Snow raise Ouch("VENV creation subprocess failed.") from exc 392dd84028fSJohn Snow 393dd84028fSJohn Snow # print the python executable to stdout for configure. 394dd84028fSJohn Snow print(builder.get_value("env_exe")) 395dd84028fSJohn Snow 396dd84028fSJohn Snow 39792834894SJohn Snowdef _gen_importlib(packages: Sequence[str]) -> Iterator[str]: 39892834894SJohn Snow # pylint: disable=import-outside-toplevel 39992834894SJohn Snow # pylint: disable=no-name-in-module 40092834894SJohn Snow # pylint: disable=import-error 40192834894SJohn Snow try: 40292834894SJohn Snow # First preference: Python 3.8+ stdlib 40392834894SJohn Snow from importlib.metadata import ( # type: ignore 40492834894SJohn Snow PackageNotFoundError, 40592834894SJohn Snow distribution, 40692834894SJohn Snow ) 40792834894SJohn Snow except ImportError as exc: 40892834894SJohn Snow logger.debug("%s", str(exc)) 40992834894SJohn Snow # Second preference: Commonly available PyPI backport 41092834894SJohn Snow from importlib_metadata import ( # type: ignore 41192834894SJohn Snow PackageNotFoundError, 41292834894SJohn Snow distribution, 41392834894SJohn Snow ) 41492834894SJohn Snow 41592834894SJohn Snow def _generator() -> Iterator[str]: 41692834894SJohn Snow for package in packages: 41792834894SJohn Snow try: 41892834894SJohn Snow entry_points = distribution(package).entry_points 41992834894SJohn Snow except PackageNotFoundError: 42092834894SJohn Snow continue 42192834894SJohn Snow 42292834894SJohn Snow # The EntryPoints type is only available in 3.10+, 42392834894SJohn Snow # treat this as a vanilla list and filter it ourselves. 42492834894SJohn Snow entry_points = filter( 42592834894SJohn Snow lambda ep: ep.group == "console_scripts", entry_points 42692834894SJohn Snow ) 42792834894SJohn Snow 42892834894SJohn Snow for entry_point in entry_points: 42992834894SJohn Snow yield f"{entry_point.name} = {entry_point.value}" 43092834894SJohn Snow 43192834894SJohn Snow return _generator() 43292834894SJohn Snow 43392834894SJohn Snow 43492834894SJohn Snowdef _gen_pkg_resources(packages: Sequence[str]) -> Iterator[str]: 43592834894SJohn Snow # pylint: disable=import-outside-toplevel 43692834894SJohn Snow # Bundled with setuptools; has a good chance of being available. 43792834894SJohn Snow import pkg_resources 43892834894SJohn Snow 43992834894SJohn Snow def _generator() -> Iterator[str]: 44092834894SJohn Snow for package in packages: 44192834894SJohn Snow try: 44292834894SJohn Snow eps = pkg_resources.get_entry_map(package, "console_scripts") 44392834894SJohn Snow except pkg_resources.DistributionNotFound: 44492834894SJohn Snow continue 44592834894SJohn Snow 44692834894SJohn Snow for entry_point in eps.values(): 44792834894SJohn Snow yield str(entry_point) 44892834894SJohn Snow 44992834894SJohn Snow return _generator() 45092834894SJohn Snow 45192834894SJohn Snow 45292834894SJohn Snowdef generate_console_scripts( 45392834894SJohn Snow packages: Sequence[str], 45492834894SJohn Snow python_path: Optional[str] = None, 45592834894SJohn Snow bin_path: Optional[str] = None, 45692834894SJohn Snow) -> None: 45792834894SJohn Snow """ 45892834894SJohn Snow Generate script shims for console_script entry points in @packages. 45992834894SJohn Snow """ 46092834894SJohn Snow if python_path is None: 46192834894SJohn Snow python_path = sys.executable 46292834894SJohn Snow if bin_path is None: 46392834894SJohn Snow bin_path = sysconfig.get_path("scripts") 46492834894SJohn Snow assert bin_path is not None 46592834894SJohn Snow 46692834894SJohn Snow logger.debug( 46792834894SJohn Snow "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)", 46892834894SJohn Snow packages, 46992834894SJohn Snow python_path, 47092834894SJohn Snow bin_path, 47192834894SJohn Snow ) 47292834894SJohn Snow 47392834894SJohn Snow if not packages: 47492834894SJohn Snow return 47592834894SJohn Snow 47692834894SJohn Snow def _get_entry_points() -> Iterator[str]: 47792834894SJohn Snow """Python 3.7 compatibility shim for iterating entry points.""" 47892834894SJohn Snow # Python 3.8+, or Python 3.7 with importlib_metadata installed. 47992834894SJohn Snow try: 48092834894SJohn Snow return _gen_importlib(packages) 48192834894SJohn Snow except ImportError as exc: 48292834894SJohn Snow logger.debug("%s", str(exc)) 48392834894SJohn Snow 48492834894SJohn Snow # Python 3.7 with setuptools installed. 48592834894SJohn Snow try: 48692834894SJohn Snow return _gen_pkg_resources(packages) 48792834894SJohn Snow except ImportError as exc: 48892834894SJohn Snow logger.debug("%s", str(exc)) 48992834894SJohn Snow raise Ouch( 49092834894SJohn Snow "Neither importlib.metadata nor pkg_resources found, " 49192834894SJohn Snow "can't generate console script shims.\n" 49292834894SJohn Snow "Use Python 3.8+, or install importlib-metadata or setuptools." 49392834894SJohn Snow ) from exc 49492834894SJohn Snow 49592834894SJohn Snow maker = distlib.scripts.ScriptMaker(None, bin_path) 49692834894SJohn Snow maker.variants = {""} 49792834894SJohn Snow maker.clobber = False 49892834894SJohn Snow 49992834894SJohn Snow for entry_point in _get_entry_points(): 50092834894SJohn Snow for filename in maker.make(entry_point): 50192834894SJohn Snow logger.debug("wrote console_script '%s'", filename) 50292834894SJohn Snow 50392834894SJohn Snow 5044695a22eSJohn Snowdef pkgname_from_depspec(dep_spec: str) -> str: 5054695a22eSJohn Snow """ 5064695a22eSJohn Snow Parse package name out of a PEP-508 depspec. 5074695a22eSJohn Snow 5084695a22eSJohn Snow See https://peps.python.org/pep-0508/#names 5094695a22eSJohn Snow """ 5104695a22eSJohn Snow match = re.match( 5114695a22eSJohn Snow r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE 5124695a22eSJohn Snow ) 5134695a22eSJohn Snow if not match: 5144695a22eSJohn Snow raise ValueError( 5154695a22eSJohn Snow f"dep_spec '{dep_spec}'" 5164695a22eSJohn Snow " does not appear to contain a valid package name" 5174695a22eSJohn Snow ) 5184695a22eSJohn Snow return match.group(0) 5194695a22eSJohn Snow 5204695a22eSJohn Snow 5214695a22eSJohn Snowdef diagnose( 5224695a22eSJohn Snow dep_spec: str, 5234695a22eSJohn Snow online: bool, 5244695a22eSJohn Snow wheels_dir: Optional[Union[str, Path]], 5254695a22eSJohn Snow prog: Optional[str], 5264695a22eSJohn Snow) -> Tuple[str, bool]: 5274695a22eSJohn Snow """ 5284695a22eSJohn Snow Offer a summary to the user as to why a package failed to be installed. 5294695a22eSJohn Snow 5304695a22eSJohn Snow :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5' 5314695a22eSJohn Snow :param online: Did we allow PyPI access? 5324695a22eSJohn Snow :param prog: 5334695a22eSJohn Snow Optionally, a shell program name that can be used as a 5344695a22eSJohn Snow bellwether to detect if this program is installed elsewhere on 5354695a22eSJohn Snow the system. This is used to offer advice when a program is 5364695a22eSJohn Snow detected for a different python version. 5374695a22eSJohn Snow :param wheels_dir: 5384695a22eSJohn Snow Optionally, a directory that was searched for vendored packages. 5394695a22eSJohn Snow """ 5404695a22eSJohn Snow # pylint: disable=too-many-branches 5414695a22eSJohn Snow 5424695a22eSJohn Snow # Some errors are not particularly serious 5434695a22eSJohn Snow bad = False 5444695a22eSJohn Snow 5454695a22eSJohn Snow pkg_name = pkgname_from_depspec(dep_spec) 5464695a22eSJohn Snow pkg_version = None 5474695a22eSJohn Snow 5484695a22eSJohn Snow has_importlib = False 5494695a22eSJohn Snow try: 5504695a22eSJohn Snow # Python 3.8+ stdlib 5514695a22eSJohn Snow # pylint: disable=import-outside-toplevel 5524695a22eSJohn Snow # pylint: disable=no-name-in-module 5534695a22eSJohn Snow # pylint: disable=import-error 5544695a22eSJohn Snow from importlib.metadata import ( # type: ignore 5554695a22eSJohn Snow PackageNotFoundError, 5564695a22eSJohn Snow version, 5574695a22eSJohn Snow ) 5584695a22eSJohn Snow 5594695a22eSJohn Snow has_importlib = True 5604695a22eSJohn Snow try: 5614695a22eSJohn Snow pkg_version = version(pkg_name) 5624695a22eSJohn Snow except PackageNotFoundError: 5634695a22eSJohn Snow pass 5644695a22eSJohn Snow except ModuleNotFoundError: 5654695a22eSJohn Snow pass 5664695a22eSJohn Snow 5674695a22eSJohn Snow lines = [] 5684695a22eSJohn Snow 5694695a22eSJohn Snow if pkg_version: 5704695a22eSJohn Snow lines.append( 5714695a22eSJohn Snow f"Python package '{pkg_name}' version '{pkg_version}' was found," 5724695a22eSJohn Snow " but isn't suitable." 5734695a22eSJohn Snow ) 5744695a22eSJohn Snow elif has_importlib: 5754695a22eSJohn Snow lines.append( 5764695a22eSJohn Snow f"Python package '{pkg_name}' was not found nor installed." 5774695a22eSJohn Snow ) 5784695a22eSJohn Snow else: 5794695a22eSJohn Snow lines.append( 5804695a22eSJohn Snow f"Python package '{pkg_name}' is either not found or" 5814695a22eSJohn Snow " not a suitable version." 5824695a22eSJohn Snow ) 5834695a22eSJohn Snow 5844695a22eSJohn Snow if wheels_dir: 5854695a22eSJohn Snow lines.append( 5864695a22eSJohn Snow "No suitable version found in, or failed to install from" 5874695a22eSJohn Snow f" '{wheels_dir}'." 5884695a22eSJohn Snow ) 5894695a22eSJohn Snow bad = True 5904695a22eSJohn Snow 5914695a22eSJohn Snow if online: 5924695a22eSJohn Snow lines.append("A suitable version could not be obtained from PyPI.") 5934695a22eSJohn Snow bad = True 5944695a22eSJohn Snow else: 5954695a22eSJohn Snow lines.append( 5964695a22eSJohn Snow "mkvenv was configured to operate offline and did not check PyPI." 5974695a22eSJohn Snow ) 5984695a22eSJohn Snow 5994695a22eSJohn Snow if prog and not pkg_version: 6004695a22eSJohn Snow which = shutil.which(prog) 6014695a22eSJohn Snow if which: 6024695a22eSJohn Snow if sys.base_prefix in site.PREFIXES: 6034695a22eSJohn Snow pypath = Path(sys.executable).resolve() 6044695a22eSJohn Snow lines.append( 6054695a22eSJohn Snow f"'{prog}' was detected on your system at '{which}', " 6064695a22eSJohn Snow f"but the Python package '{pkg_name}' was not found by " 6074695a22eSJohn Snow f"this Python interpreter ('{pypath}'). " 6084695a22eSJohn Snow f"Typically this means that '{prog}' has been installed " 6094695a22eSJohn Snow "against a different Python interpreter on your system." 6104695a22eSJohn Snow ) 6114695a22eSJohn Snow else: 6124695a22eSJohn Snow lines.append( 6134695a22eSJohn Snow f"'{prog}' was detected on your system at '{which}', " 6144695a22eSJohn Snow "but the build is using an isolated virtual environment." 6154695a22eSJohn Snow ) 6164695a22eSJohn Snow bad = True 6174695a22eSJohn Snow 6184695a22eSJohn Snow lines = [f" • {line}" for line in lines] 6194695a22eSJohn Snow if bad: 6204695a22eSJohn Snow lines.insert(0, f"Could not provide build dependency '{dep_spec}':") 6214695a22eSJohn Snow else: 6224695a22eSJohn Snow lines.insert(0, f"'{dep_spec}' not found:") 6234695a22eSJohn Snow return os.linesep.join(lines), bad 6244695a22eSJohn Snow 6254695a22eSJohn Snow 626c5538eedSJohn Snowdef pip_install( 627c5538eedSJohn Snow args: Sequence[str], 628c5538eedSJohn Snow online: bool = False, 629c5538eedSJohn Snow wheels_dir: Optional[Union[str, Path]] = None, 630c5538eedSJohn Snow) -> None: 631c5538eedSJohn Snow """ 632c5538eedSJohn Snow Use pip to install a package or package(s) as specified in @args. 633c5538eedSJohn Snow """ 634c5538eedSJohn Snow loud = bool( 635c5538eedSJohn Snow os.environ.get("DEBUG") 636c5538eedSJohn Snow or os.environ.get("GITLAB_CI") 637c5538eedSJohn Snow or os.environ.get("V") 638c5538eedSJohn Snow ) 639c5538eedSJohn Snow 640c5538eedSJohn Snow full_args = [ 641c5538eedSJohn Snow sys.executable, 642c5538eedSJohn Snow "-m", 643c5538eedSJohn Snow "pip", 644c5538eedSJohn Snow "install", 645c5538eedSJohn Snow "--disable-pip-version-check", 646c5538eedSJohn Snow "-v" if loud else "-q", 647c5538eedSJohn Snow ] 648c5538eedSJohn Snow if not online: 649c5538eedSJohn Snow full_args += ["--no-index"] 650c5538eedSJohn Snow if wheels_dir: 651c5538eedSJohn Snow full_args += ["--find-links", f"file://{str(wheels_dir)}"] 652c5538eedSJohn Snow full_args += list(args) 653c5538eedSJohn Snow subprocess.run( 654c5538eedSJohn Snow full_args, 655c5538eedSJohn Snow check=True, 656c5538eedSJohn Snow ) 657c5538eedSJohn Snow 658c5538eedSJohn Snow 6594695a22eSJohn Snowdef _do_ensure( 660c5538eedSJohn Snow dep_specs: Sequence[str], 661c5538eedSJohn Snow online: bool = False, 662c5538eedSJohn Snow wheels_dir: Optional[Union[str, Path]] = None, 663c5538eedSJohn Snow) -> None: 664c5538eedSJohn Snow """ 665c5538eedSJohn Snow Use pip to ensure we have the package specified by @dep_specs. 666c5538eedSJohn Snow 667c5538eedSJohn Snow If the package is already installed, do nothing. If online and 668c5538eedSJohn Snow wheels_dir are both provided, prefer packages found in wheels_dir 669c5538eedSJohn Snow first before connecting to PyPI. 670c5538eedSJohn Snow 671c5538eedSJohn Snow :param dep_specs: 672c5538eedSJohn Snow PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. 673c5538eedSJohn Snow :param online: If True, fall back to PyPI. 674c5538eedSJohn Snow :param wheels_dir: If specified, search this path for packages. 675c5538eedSJohn Snow """ 676c5538eedSJohn Snow with warnings.catch_warnings(): 677c5538eedSJohn Snow warnings.filterwarnings( 678c5538eedSJohn Snow "ignore", category=UserWarning, module="distlib" 679c5538eedSJohn Snow ) 680c5538eedSJohn Snow dist_path = distlib.database.DistributionPath(include_egg=True) 681c5538eedSJohn Snow absent = [] 68292834894SJohn Snow present = [] 683c5538eedSJohn Snow for spec in dep_specs: 684c5538eedSJohn Snow matcher = distlib.version.LegacyMatcher(spec) 685c5538eedSJohn Snow dist = dist_path.get_distribution(matcher.name) 686c5538eedSJohn Snow if dist is None or not matcher.match(dist.version): 687c5538eedSJohn Snow absent.append(spec) 688c5538eedSJohn Snow else: 689c5538eedSJohn Snow logger.info("found %s", dist) 69092834894SJohn Snow present.append(matcher.name) 69192834894SJohn Snow 69292834894SJohn Snow if present: 69392834894SJohn Snow generate_console_scripts(present) 694c5538eedSJohn Snow 695c5538eedSJohn Snow if absent: 696c5538eedSJohn Snow # Some packages are missing or aren't a suitable version, 697c5538eedSJohn Snow # install a suitable (possibly vendored) package. 698c5538eedSJohn Snow print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr) 699c5538eedSJohn Snow pip_install(args=absent, online=online, wheels_dir=wheels_dir) 700c5538eedSJohn Snow 701c5538eedSJohn Snow 7024695a22eSJohn Snowdef ensure( 7034695a22eSJohn Snow dep_specs: Sequence[str], 7044695a22eSJohn Snow online: bool = False, 7054695a22eSJohn Snow wheels_dir: Optional[Union[str, Path]] = None, 7064695a22eSJohn Snow prog: Optional[str] = None, 7074695a22eSJohn Snow) -> None: 7084695a22eSJohn Snow """ 7094695a22eSJohn Snow Use pip to ensure we have the package specified by @dep_specs. 7104695a22eSJohn Snow 7114695a22eSJohn Snow If the package is already installed, do nothing. If online and 7124695a22eSJohn Snow wheels_dir are both provided, prefer packages found in wheels_dir 7134695a22eSJohn Snow first before connecting to PyPI. 7144695a22eSJohn Snow 7154695a22eSJohn Snow :param dep_specs: 7164695a22eSJohn Snow PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. 7174695a22eSJohn Snow :param online: If True, fall back to PyPI. 7184695a22eSJohn Snow :param wheels_dir: If specified, search this path for packages. 7194695a22eSJohn Snow :param prog: 7204695a22eSJohn Snow If specified, use this program name for error diagnostics that will 7214695a22eSJohn Snow be presented to the user. e.g., 'sphinx-build' can be used as a 7224695a22eSJohn Snow bellwether for the presence of 'sphinx'. 7234695a22eSJohn Snow """ 7244695a22eSJohn Snow print(f"mkvenv: checking for {', '.join(dep_specs)}", file=sys.stderr) 72568ea6d17SJohn Snow 72668ea6d17SJohn Snow if not HAVE_DISTLIB: 72768ea6d17SJohn Snow raise Ouch("a usable distlib could not be found, please install it") 72868ea6d17SJohn Snow 7294695a22eSJohn Snow try: 7304695a22eSJohn Snow _do_ensure(dep_specs, online, wheels_dir) 7314695a22eSJohn Snow except subprocess.CalledProcessError as exc: 7324695a22eSJohn Snow # Well, that's not good. 7334695a22eSJohn Snow msg, bad = diagnose(dep_specs[0], online, wheels_dir, prog) 7344695a22eSJohn Snow if bad: 7354695a22eSJohn Snow raise Ouch(msg) from exc 7364695a22eSJohn Snow raise SystemExit(f"\n{msg}\n\n") from exc 7374695a22eSJohn Snow 7384695a22eSJohn Snow 739*f1ad527fSJohn Snowdef post_venv_setup() -> None: 740*f1ad527fSJohn Snow """ 741*f1ad527fSJohn Snow This is intended to be run *inside the venv* after it is created. 742*f1ad527fSJohn Snow """ 743*f1ad527fSJohn Snow logger.debug("post_venv_setup()") 744*f1ad527fSJohn Snow # Generate a 'pip' script so the venv is usable in a normal 745*f1ad527fSJohn Snow # way from the CLI. This only happens when we inherited pip from a 746*f1ad527fSJohn Snow # parent/system-site and haven't run ensurepip in some way. 747*f1ad527fSJohn Snow generate_console_scripts(["pip"]) 748*f1ad527fSJohn Snow 749*f1ad527fSJohn Snow 750dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None: 751dd84028fSJohn Snow subparser = subparsers.add_parser("create", help="create a venv") 752dd84028fSJohn Snow subparser.add_argument( 753dd84028fSJohn Snow "target", 754dd84028fSJohn Snow type=str, 755dd84028fSJohn Snow action="store", 756dd84028fSJohn Snow help="Target directory to install virtual environment into.", 757dd84028fSJohn Snow ) 758dd84028fSJohn Snow 759dd84028fSJohn Snow 760*f1ad527fSJohn Snowdef _add_post_init_subcommand(subparsers: Any) -> None: 761*f1ad527fSJohn Snow subparsers.add_parser("post_init", help="post-venv initialization") 762*f1ad527fSJohn Snow 763*f1ad527fSJohn Snow 764c5538eedSJohn Snowdef _add_ensure_subcommand(subparsers: Any) -> None: 765c5538eedSJohn Snow subparser = subparsers.add_parser( 766c5538eedSJohn Snow "ensure", help="Ensure that the specified package is installed." 767c5538eedSJohn Snow ) 768c5538eedSJohn Snow subparser.add_argument( 769c5538eedSJohn Snow "--online", 770c5538eedSJohn Snow action="store_true", 771c5538eedSJohn Snow help="Install packages from PyPI, if necessary.", 772c5538eedSJohn Snow ) 773c5538eedSJohn Snow subparser.add_argument( 774c5538eedSJohn Snow "--dir", 775c5538eedSJohn Snow type=str, 776c5538eedSJohn Snow action="store", 777c5538eedSJohn Snow help="Path to vendored packages where we may install from.", 778c5538eedSJohn Snow ) 779c5538eedSJohn Snow subparser.add_argument( 7804695a22eSJohn Snow "--diagnose", 7814695a22eSJohn Snow type=str, 7824695a22eSJohn Snow action="store", 7834695a22eSJohn Snow help=( 7844695a22eSJohn Snow "Name of a shell utility to use for " 7854695a22eSJohn Snow "diagnostics if this command fails." 7864695a22eSJohn Snow ), 7874695a22eSJohn Snow ) 7884695a22eSJohn Snow subparser.add_argument( 789c5538eedSJohn Snow "dep_specs", 790c5538eedSJohn Snow type=str, 791c5538eedSJohn Snow action="store", 792c5538eedSJohn Snow help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'", 793c5538eedSJohn Snow nargs="+", 794c5538eedSJohn Snow ) 795c5538eedSJohn Snow 796c5538eedSJohn Snow 797dd84028fSJohn Snowdef main() -> int: 798dd84028fSJohn Snow """CLI interface to make_qemu_venv. See module docstring.""" 799dd84028fSJohn Snow if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"): 800dd84028fSJohn Snow # You're welcome. 801dd84028fSJohn Snow logging.basicConfig(level=logging.DEBUG) 802c5538eedSJohn Snow else: 803c5538eedSJohn Snow if os.environ.get("V"): 804dd84028fSJohn Snow logging.basicConfig(level=logging.INFO) 805dd84028fSJohn Snow 806c5538eedSJohn Snow # These are incredibly noisy even for V=1 807c5538eedSJohn Snow logging.getLogger("distlib.metadata").addFilter(lambda record: False) 808c5538eedSJohn Snow logging.getLogger("distlib.database").addFilter(lambda record: False) 809c5538eedSJohn Snow 810dd84028fSJohn Snow parser = argparse.ArgumentParser( 811dd84028fSJohn Snow prog="mkvenv", 812dd84028fSJohn Snow description="QEMU pyvenv bootstrapping utility", 813dd84028fSJohn Snow ) 814dd84028fSJohn Snow subparsers = parser.add_subparsers( 815dd84028fSJohn Snow title="Commands", 816dd84028fSJohn Snow dest="command", 817dd84028fSJohn Snow metavar="command", 818dd84028fSJohn Snow help="Description", 819dd84028fSJohn Snow ) 820dd84028fSJohn Snow 821dd84028fSJohn Snow _add_create_subcommand(subparsers) 822*f1ad527fSJohn Snow _add_post_init_subcommand(subparsers) 823c5538eedSJohn Snow _add_ensure_subcommand(subparsers) 824dd84028fSJohn Snow 825dd84028fSJohn Snow args = parser.parse_args() 826dd84028fSJohn Snow try: 827dd84028fSJohn Snow if args.command == "create": 828dd84028fSJohn Snow make_venv( 829dd84028fSJohn Snow args.target, 830dd84028fSJohn Snow system_site_packages=True, 831dd84028fSJohn Snow clear=True, 832dd84028fSJohn Snow ) 833*f1ad527fSJohn Snow if args.command == "post_init": 834*f1ad527fSJohn Snow post_venv_setup() 835c5538eedSJohn Snow if args.command == "ensure": 836c5538eedSJohn Snow ensure( 837c5538eedSJohn Snow dep_specs=args.dep_specs, 838c5538eedSJohn Snow online=args.online, 839c5538eedSJohn Snow wheels_dir=args.dir, 8404695a22eSJohn Snow prog=args.diagnose, 841c5538eedSJohn Snow ) 842dd84028fSJohn Snow logger.debug("mkvenv.py %s: exiting", args.command) 843dd84028fSJohn Snow except Ouch as exc: 844dd84028fSJohn Snow print("\n*** Ouch! ***\n", file=sys.stderr) 845dd84028fSJohn Snow print(str(exc), "\n\n", file=sys.stderr) 846dd84028fSJohn Snow return 1 847dd84028fSJohn Snow except SystemExit: 848dd84028fSJohn Snow raise 849dd84028fSJohn Snow except: # pylint: disable=bare-except 850dd84028fSJohn Snow logger.exception("mkvenv did not complete successfully:") 851dd84028fSJohn Snow return 2 852dd84028fSJohn Snow return 0 853dd84028fSJohn Snow 854dd84028fSJohn Snow 855dd84028fSJohn Snowif __name__ == "__main__": 856dd84028fSJohn Snow sys.exit(main()) 857