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 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 30f1ad527fSJohn Snowusage: mkvenv post_init [-h] 31f1ad527fSJohn Snow 32f1ad527fSJohn Snowoptions: 33f1ad527fSJohn Snow -h, --help show this help message and exit 34f1ad527fSJohn Snow 35f1ad527fSJohn Snow-------------------------------------------------- 36f1ad527fSJohn 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 Snow 8068ea6d17SJohn Snow 8168ea6d17SJohn Snow# Try to load distlib, with a fallback to pip's vendored version. 8268ea6d17SJohn Snow# HAVE_DISTLIB is checked below, just-in-time, so that mkvenv does not fail 8368ea6d17SJohn Snow# outside the venv or before a potential call to ensurepip in checkpip(). 8468ea6d17SJohn SnowHAVE_DISTLIB = True 8568ea6d17SJohn Snowtry: 8692834894SJohn Snow import distlib.scripts 87c5538eedSJohn Snow import distlib.version 8868ea6d17SJohn Snowexcept ImportError: 8968ea6d17SJohn Snow try: 9068ea6d17SJohn Snow # Reach into pip's cookie jar. pylint and flake8 don't understand 9168ea6d17SJohn Snow # that these imports will be used via distlib.xxx. 9268ea6d17SJohn Snow from pip._vendor import distlib 9368ea6d17SJohn Snow import pip._vendor.distlib.scripts # noqa, pylint: disable=unused-import 9468ea6d17SJohn Snow import pip._vendor.distlib.version # noqa, pylint: disable=unused-import 9568ea6d17SJohn Snow except ImportError: 9668ea6d17SJohn Snow HAVE_DISTLIB = False 97dd84028fSJohn Snow 98dd84028fSJohn Snow# Do not add any mandatory dependencies from outside the stdlib: 99dd84028fSJohn Snow# This script *must* be usable standalone! 100dd84028fSJohn Snow 101dd84028fSJohn SnowDirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] 102dd84028fSJohn Snowlogger = logging.getLogger("mkvenv") 103dd84028fSJohn Snow 104dd84028fSJohn Snow 105dee01b82SJohn Snowdef inside_a_venv() -> bool: 106dee01b82SJohn Snow """Returns True if it is executed inside of a virtual environment.""" 107dee01b82SJohn Snow return sys.prefix != sys.base_prefix 108dee01b82SJohn Snow 109dee01b82SJohn Snow 110dd84028fSJohn Snowclass Ouch(RuntimeError): 111dd84028fSJohn Snow """An Exception class we can't confuse with a builtin.""" 112dd84028fSJohn Snow 113dd84028fSJohn Snow 114dd84028fSJohn Snowclass QemuEnvBuilder(venv.EnvBuilder): 115dd84028fSJohn Snow """ 116dd84028fSJohn Snow An extension of venv.EnvBuilder for building QEMU's configure-time venv. 117dd84028fSJohn Snow 118dee01b82SJohn Snow The primary difference is that it emulates a "nested" virtual 119dee01b82SJohn Snow environment when invoked from inside of an existing virtual 120f1ad527fSJohn Snow environment by including packages from the parent. Also, 121f1ad527fSJohn Snow "ensurepip" is replaced if possible with just recreating pip's 122f1ad527fSJohn Snow console_scripts inside the virtual environment. 123dd84028fSJohn Snow 124dd84028fSJohn Snow Parameters for base class init: 125dd84028fSJohn Snow - system_site_packages: bool = False 126dd84028fSJohn Snow - clear: bool = False 127dd84028fSJohn Snow - symlinks: bool = False 128dd84028fSJohn Snow - upgrade: bool = False 129dd84028fSJohn Snow - with_pip: bool = False 130dd84028fSJohn Snow - prompt: Optional[str] = None 131dd84028fSJohn Snow - upgrade_deps: bool = False (Since 3.9) 132dd84028fSJohn Snow """ 133dd84028fSJohn Snow 134dd84028fSJohn Snow def __init__(self, *args: Any, **kwargs: Any) -> None: 135dd84028fSJohn Snow logger.debug("QemuEnvBuilder.__init__(...)") 136a9dbde71SJohn Snow 137dee01b82SJohn Snow # For nested venv emulation: 138dee01b82SJohn Snow self.use_parent_packages = False 139dee01b82SJohn Snow if inside_a_venv(): 140dee01b82SJohn Snow # Include parent packages only if we're in a venv and 141dee01b82SJohn Snow # system_site_packages was True. 142dee01b82SJohn Snow self.use_parent_packages = kwargs.pop( 143dee01b82SJohn Snow "system_site_packages", False 144dee01b82SJohn Snow ) 145dee01b82SJohn Snow # Include system_site_packages only when the parent, 146dee01b82SJohn Snow # The venv we are currently in, also does so. 147dee01b82SJohn Snow kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES 148dee01b82SJohn Snow 149f1ad527fSJohn Snow # ensurepip is slow: venv creation can be very fast for cases where 150f1ad527fSJohn Snow # we allow the use of system_site_packages. Therefore, ensurepip is 151f1ad527fSJohn Snow # replaced with our own script generation once the virtual environment 152f1ad527fSJohn Snow # is setup. 153f1ad527fSJohn Snow self.want_pip = kwargs.get("with_pip", False) 154f1ad527fSJohn Snow if self.want_pip: 155f1ad527fSJohn Snow if ( 156f1ad527fSJohn Snow kwargs.get("system_site_packages", False) 157f1ad527fSJohn Snow and not need_ensurepip() 158f1ad527fSJohn Snow ): 159f1ad527fSJohn Snow kwargs["with_pip"] = False 160f1ad527fSJohn Snow else: 161c8049626SJohn Snow check_ensurepip(suggest_remedy=True) 162a9dbde71SJohn Snow 163dd84028fSJohn Snow super().__init__(*args, **kwargs) 164dd84028fSJohn Snow 165dd84028fSJohn Snow # Make the context available post-creation: 166dd84028fSJohn Snow self._context: Optional[SimpleNamespace] = None 167dd84028fSJohn Snow 168dee01b82SJohn Snow def get_parent_libpath(self) -> Optional[str]: 169dee01b82SJohn Snow """Return the libpath of the parent venv, if applicable.""" 170dee01b82SJohn Snow if self.use_parent_packages: 171dee01b82SJohn Snow return sysconfig.get_path("purelib") 172dee01b82SJohn Snow return None 173dee01b82SJohn Snow 174dee01b82SJohn Snow @staticmethod 175dee01b82SJohn Snow def compute_venv_libpath(context: SimpleNamespace) -> str: 176dee01b82SJohn Snow """ 177dee01b82SJohn Snow Compatibility wrapper for context.lib_path for Python < 3.12 178dee01b82SJohn Snow """ 179dee01b82SJohn Snow # Python 3.12+, not strictly necessary because it's documented 180dee01b82SJohn Snow # to be the same as 3.10 code below: 181dee01b82SJohn Snow if sys.version_info >= (3, 12): 182dee01b82SJohn Snow return context.lib_path 183dee01b82SJohn Snow 184dee01b82SJohn Snow # Python 3.10+ 185dee01b82SJohn Snow if "venv" in sysconfig.get_scheme_names(): 186dee01b82SJohn Snow lib_path = sysconfig.get_path( 187dee01b82SJohn Snow "purelib", scheme="venv", vars={"base": context.env_dir} 188dee01b82SJohn Snow ) 189dee01b82SJohn Snow assert lib_path is not None 190dee01b82SJohn Snow return lib_path 191dee01b82SJohn Snow 192dee01b82SJohn Snow # For Python <= 3.9 we need to hardcode this. Fortunately the 193dee01b82SJohn Snow # code below was the same in Python 3.6-3.10, so there is only 194dee01b82SJohn Snow # one case. 195dee01b82SJohn Snow if sys.platform == "win32": 196dee01b82SJohn Snow return os.path.join(context.env_dir, "Lib", "site-packages") 197dee01b82SJohn Snow return os.path.join( 198dee01b82SJohn Snow context.env_dir, 199dee01b82SJohn Snow "lib", 200dee01b82SJohn Snow "python%d.%d" % sys.version_info[:2], 201dee01b82SJohn Snow "site-packages", 202dee01b82SJohn Snow ) 203dee01b82SJohn Snow 204dd84028fSJohn Snow def ensure_directories(self, env_dir: DirType) -> SimpleNamespace: 205dd84028fSJohn Snow logger.debug("ensure_directories(env_dir=%s)", env_dir) 206dd84028fSJohn Snow self._context = super().ensure_directories(env_dir) 207dd84028fSJohn Snow return self._context 208dd84028fSJohn Snow 209dee01b82SJohn Snow def create(self, env_dir: DirType) -> None: 210dee01b82SJohn Snow logger.debug("create(env_dir=%s)", env_dir) 211dee01b82SJohn Snow super().create(env_dir) 212dee01b82SJohn Snow assert self._context is not None 213dee01b82SJohn Snow self.post_post_setup(self._context) 214dee01b82SJohn Snow 215dee01b82SJohn Snow def post_post_setup(self, context: SimpleNamespace) -> None: 216dee01b82SJohn Snow """ 217dee01b82SJohn Snow The final, final hook. Enter the venv and run commands inside of it. 218dee01b82SJohn Snow """ 219dee01b82SJohn Snow if self.use_parent_packages: 220dee01b82SJohn Snow # We're inside of a venv and we want to include the parent 221dee01b82SJohn Snow # venv's packages. 222dee01b82SJohn Snow parent_libpath = self.get_parent_libpath() 223dee01b82SJohn Snow assert parent_libpath is not None 224dee01b82SJohn Snow logger.debug("parent_libpath: %s", parent_libpath) 225dee01b82SJohn Snow 226dee01b82SJohn Snow our_libpath = self.compute_venv_libpath(context) 227dee01b82SJohn Snow logger.debug("our_libpath: %s", our_libpath) 228dee01b82SJohn Snow 229dee01b82SJohn Snow pth_file = os.path.join(our_libpath, "nested.pth") 230dee01b82SJohn Snow with open(pth_file, "w", encoding="UTF-8") as file: 231dee01b82SJohn Snow file.write(parent_libpath + os.linesep) 232dee01b82SJohn Snow 233f1ad527fSJohn Snow if self.want_pip: 234f1ad527fSJohn Snow args = [ 235f1ad527fSJohn Snow context.env_exe, 236f1ad527fSJohn Snow __file__, 237f1ad527fSJohn Snow "post_init", 238f1ad527fSJohn Snow ] 239f1ad527fSJohn Snow subprocess.run(args, check=True) 240f1ad527fSJohn Snow 241dd84028fSJohn Snow def get_value(self, field: str) -> str: 242dd84028fSJohn Snow """ 243dd84028fSJohn Snow Get a string value from the context namespace after a call to build. 244dd84028fSJohn Snow 245dd84028fSJohn Snow For valid field names, see: 246dd84028fSJohn Snow https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories 247dd84028fSJohn Snow """ 248dd84028fSJohn Snow ret = getattr(self._context, field) 249dd84028fSJohn Snow assert isinstance(ret, str) 250dd84028fSJohn Snow return ret 251dd84028fSJohn Snow 252dd84028fSJohn Snow 253f1ad527fSJohn Snowdef need_ensurepip() -> bool: 254f1ad527fSJohn Snow """ 255f1ad527fSJohn Snow Tests for the presence of setuptools and pip. 256f1ad527fSJohn Snow 257f1ad527fSJohn Snow :return: `True` if we do not detect both packages. 258f1ad527fSJohn Snow """ 259f1ad527fSJohn Snow # Don't try to actually import them, it's fraught with danger: 260f1ad527fSJohn Snow # https://github.com/pypa/setuptools/issues/2993 261f1ad527fSJohn Snow if find_spec("setuptools") and find_spec("pip"): 262f1ad527fSJohn Snow return False 263f1ad527fSJohn Snow return True 264f1ad527fSJohn Snow 265f1ad527fSJohn Snow 266c8049626SJohn Snowdef check_ensurepip(prefix: str = "", suggest_remedy: bool = False) -> None: 267a9dbde71SJohn Snow """ 268a9dbde71SJohn Snow Check that we have ensurepip. 269a9dbde71SJohn Snow 270a9dbde71SJohn Snow Raise a fatal exception with a helpful hint if it isn't available. 271a9dbde71SJohn Snow """ 272a9dbde71SJohn Snow if not find_spec("ensurepip"): 273a9dbde71SJohn Snow msg = ( 274a9dbde71SJohn Snow "Python's ensurepip module is not found.\n" 275a9dbde71SJohn Snow "It's normally part of the Python standard library, " 276a9dbde71SJohn Snow "maybe your distribution packages it separately?\n" 277c8049626SJohn Snow "(Debian puts ensurepip in its python3-venv package.)\n" 278c8049626SJohn Snow ) 279c8049626SJohn Snow if suggest_remedy: 280c8049626SJohn Snow msg += ( 281a9dbde71SJohn Snow "Either install ensurepip, or alleviate the need for it in the" 282a9dbde71SJohn Snow " first place by installing pip and setuptools for " 283a9dbde71SJohn Snow f"'{sys.executable}'.\n" 284a9dbde71SJohn Snow ) 285c8049626SJohn Snow raise Ouch(prefix + 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" 293c8049626SJohn Snow "(NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)\n" 294c8049626SJohn Snow ) 295c8049626SJohn Snow if suggest_remedy: 296c8049626SJohn Snow msg += ( 297a9dbde71SJohn Snow "Either install pyexpat, or alleviate the need for it in the " 298a9dbde71SJohn Snow "first place by installing pip and setuptools for " 299c8049626SJohn Snow f"'{sys.executable}'.\n" 300a9dbde71SJohn Snow ) 301c8049626SJohn Snow raise Ouch(prefix + msg) 302a9dbde71SJohn Snow 303a9dbde71SJohn Snow 304dd84028fSJohn Snowdef make_venv( # pylint: disable=too-many-arguments 305dd84028fSJohn Snow env_dir: Union[str, Path], 306dd84028fSJohn Snow system_site_packages: bool = False, 307dd84028fSJohn Snow clear: bool = True, 308dd84028fSJohn Snow symlinks: Optional[bool] = None, 309dd84028fSJohn Snow with_pip: bool = True, 310dd84028fSJohn Snow) -> None: 311dd84028fSJohn Snow """ 312dd84028fSJohn Snow Create a venv using `QemuEnvBuilder`. 313dd84028fSJohn Snow 314dd84028fSJohn Snow This is analogous to the `venv.create` module-level convenience 315dd84028fSJohn Snow function that is part of the Python stdblib, except it uses 316dd84028fSJohn Snow `QemuEnvBuilder` instead. 317dd84028fSJohn Snow 318dd84028fSJohn Snow :param env_dir: The directory to create/install to. 319dd84028fSJohn Snow :param system_site_packages: 320dd84028fSJohn Snow Allow inheriting packages from the system installation. 321dd84028fSJohn Snow :param clear: When True, fully remove any prior venv and files. 322dd84028fSJohn Snow :param symlinks: 323dd84028fSJohn Snow Whether to use symlinks to the target interpreter or not. If 324dd84028fSJohn Snow left unspecified, it will use symlinks except on Windows to 325dd84028fSJohn Snow match behavior with the "venv" CLI tool. 326dd84028fSJohn Snow :param with_pip: 327dd84028fSJohn Snow Whether to install "pip" binaries or not. 328dd84028fSJohn Snow """ 329dd84028fSJohn Snow logger.debug( 330dd84028fSJohn Snow "%s: make_venv(env_dir=%s, system_site_packages=%s, " 331dd84028fSJohn Snow "clear=%s, symlinks=%s, with_pip=%s)", 332dd84028fSJohn Snow __file__, 333dd84028fSJohn Snow str(env_dir), 334dd84028fSJohn Snow system_site_packages, 335dd84028fSJohn Snow clear, 336dd84028fSJohn Snow symlinks, 337dd84028fSJohn Snow with_pip, 338dd84028fSJohn Snow ) 339dd84028fSJohn Snow 340dd84028fSJohn Snow if symlinks is None: 341dd84028fSJohn Snow # Default behavior of standard venv CLI 342dd84028fSJohn Snow symlinks = os.name != "nt" 343dd84028fSJohn Snow 344dd84028fSJohn Snow builder = QemuEnvBuilder( 345dd84028fSJohn Snow system_site_packages=system_site_packages, 346dd84028fSJohn Snow clear=clear, 347dd84028fSJohn Snow symlinks=symlinks, 348dd84028fSJohn Snow with_pip=with_pip, 349dd84028fSJohn Snow ) 350dd84028fSJohn Snow 351dd84028fSJohn Snow style = "non-isolated" if builder.system_site_packages else "isolated" 352dee01b82SJohn Snow nested = "" 353dee01b82SJohn Snow if builder.use_parent_packages: 354dee01b82SJohn Snow nested = f"(with packages from '{builder.get_parent_libpath()}') " 355dd84028fSJohn Snow print( 356dd84028fSJohn Snow f"mkvenv: Creating {style} virtual environment" 357dee01b82SJohn Snow f" {nested}at '{str(env_dir)}'", 358dd84028fSJohn Snow file=sys.stderr, 359dd84028fSJohn Snow ) 360dd84028fSJohn Snow 361dd84028fSJohn Snow try: 362dd84028fSJohn Snow logger.debug("Invoking builder.create()") 363dd84028fSJohn Snow try: 364dd84028fSJohn Snow builder.create(str(env_dir)) 365dd84028fSJohn Snow except SystemExit as exc: 366dd84028fSJohn Snow # Some versions of the venv module raise SystemExit; *nasty*! 367dd84028fSJohn Snow # We want the exception that prompted it. It might be a subprocess 368dd84028fSJohn Snow # error that has output we *really* want to see. 369dd84028fSJohn Snow logger.debug("Intercepted SystemExit from EnvBuilder.create()") 370dd84028fSJohn Snow raise exc.__cause__ or exc.__context__ or exc 371dd84028fSJohn Snow logger.debug("builder.create() finished") 372dd84028fSJohn Snow except subprocess.CalledProcessError as exc: 373dd84028fSJohn Snow logger.error("mkvenv subprocess failed:") 374dd84028fSJohn Snow logger.error("cmd: %s", exc.cmd) 375dd84028fSJohn Snow logger.error("returncode: %d", exc.returncode) 376dd84028fSJohn Snow 377dd84028fSJohn Snow def _stringify(data: Union[str, bytes]) -> str: 378dd84028fSJohn Snow if isinstance(data, bytes): 379dd84028fSJohn Snow return data.decode() 380dd84028fSJohn Snow return data 381dd84028fSJohn Snow 382dd84028fSJohn Snow lines = [] 383dd84028fSJohn Snow if exc.stdout: 384dd84028fSJohn Snow lines.append("========== stdout ==========") 385dd84028fSJohn Snow lines.append(_stringify(exc.stdout)) 386dd84028fSJohn Snow lines.append("============================") 387dd84028fSJohn Snow if exc.stderr: 388dd84028fSJohn Snow lines.append("========== stderr ==========") 389dd84028fSJohn Snow lines.append(_stringify(exc.stderr)) 390dd84028fSJohn Snow lines.append("============================") 391dd84028fSJohn Snow if lines: 392dd84028fSJohn Snow logger.error(os.linesep.join(lines)) 393dd84028fSJohn Snow 394dd84028fSJohn Snow raise Ouch("VENV creation subprocess failed.") from exc 395dd84028fSJohn Snow 396dd84028fSJohn Snow # print the python executable to stdout for configure. 397dd84028fSJohn Snow print(builder.get_value("env_exe")) 398dd84028fSJohn Snow 399dd84028fSJohn Snow 40092834894SJohn Snowdef _gen_importlib(packages: Sequence[str]) -> Iterator[str]: 40192834894SJohn Snow # pylint: disable=import-outside-toplevel 40292834894SJohn Snow # pylint: disable=no-name-in-module 40392834894SJohn Snow # pylint: disable=import-error 40492834894SJohn Snow try: 40592834894SJohn Snow # First preference: Python 3.8+ stdlib 40692834894SJohn Snow from importlib.metadata import ( # type: ignore 40792834894SJohn Snow PackageNotFoundError, 40892834894SJohn Snow distribution, 40992834894SJohn Snow ) 41092834894SJohn Snow except ImportError as exc: 41192834894SJohn Snow logger.debug("%s", str(exc)) 41292834894SJohn Snow # Second preference: Commonly available PyPI backport 41392834894SJohn Snow from importlib_metadata import ( # type: ignore 41492834894SJohn Snow PackageNotFoundError, 41592834894SJohn Snow distribution, 41692834894SJohn Snow ) 41792834894SJohn Snow 41892834894SJohn Snow def _generator() -> Iterator[str]: 41992834894SJohn Snow for package in packages: 42092834894SJohn Snow try: 42192834894SJohn Snow entry_points = distribution(package).entry_points 42292834894SJohn Snow except PackageNotFoundError: 42392834894SJohn Snow continue 42492834894SJohn Snow 42592834894SJohn Snow # The EntryPoints type is only available in 3.10+, 42692834894SJohn Snow # treat this as a vanilla list and filter it ourselves. 42792834894SJohn Snow entry_points = filter( 42892834894SJohn Snow lambda ep: ep.group == "console_scripts", entry_points 42992834894SJohn Snow ) 43092834894SJohn Snow 43192834894SJohn Snow for entry_point in entry_points: 43292834894SJohn Snow yield f"{entry_point.name} = {entry_point.value}" 43392834894SJohn Snow 43492834894SJohn Snow return _generator() 43592834894SJohn Snow 43692834894SJohn Snow 43792834894SJohn Snowdef _gen_pkg_resources(packages: Sequence[str]) -> Iterator[str]: 43892834894SJohn Snow # pylint: disable=import-outside-toplevel 43992834894SJohn Snow # Bundled with setuptools; has a good chance of being available. 44092834894SJohn Snow import pkg_resources 44192834894SJohn Snow 44292834894SJohn Snow def _generator() -> Iterator[str]: 44392834894SJohn Snow for package in packages: 44492834894SJohn Snow try: 44592834894SJohn Snow eps = pkg_resources.get_entry_map(package, "console_scripts") 44692834894SJohn Snow except pkg_resources.DistributionNotFound: 44792834894SJohn Snow continue 44892834894SJohn Snow 44992834894SJohn Snow for entry_point in eps.values(): 45092834894SJohn Snow yield str(entry_point) 45192834894SJohn Snow 45292834894SJohn Snow return _generator() 45392834894SJohn Snow 45492834894SJohn Snow 45592834894SJohn Snowdef generate_console_scripts( 45692834894SJohn Snow packages: Sequence[str], 45792834894SJohn Snow python_path: Optional[str] = None, 45892834894SJohn Snow bin_path: Optional[str] = None, 45992834894SJohn Snow) -> None: 46092834894SJohn Snow """ 46192834894SJohn Snow Generate script shims for console_script entry points in @packages. 46292834894SJohn Snow """ 46392834894SJohn Snow if python_path is None: 46492834894SJohn Snow python_path = sys.executable 46592834894SJohn Snow if bin_path is None: 46692834894SJohn Snow bin_path = sysconfig.get_path("scripts") 46792834894SJohn Snow assert bin_path is not None 46892834894SJohn Snow 46992834894SJohn Snow logger.debug( 47092834894SJohn Snow "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)", 47192834894SJohn Snow packages, 47292834894SJohn Snow python_path, 47392834894SJohn Snow bin_path, 47492834894SJohn Snow ) 47592834894SJohn Snow 47692834894SJohn Snow if not packages: 47792834894SJohn Snow return 47892834894SJohn Snow 47992834894SJohn Snow def _get_entry_points() -> Iterator[str]: 48092834894SJohn Snow """Python 3.7 compatibility shim for iterating entry points.""" 48192834894SJohn Snow # Python 3.8+, or Python 3.7 with importlib_metadata installed. 48292834894SJohn Snow try: 48392834894SJohn Snow return _gen_importlib(packages) 48492834894SJohn Snow except ImportError as exc: 48592834894SJohn Snow logger.debug("%s", str(exc)) 48692834894SJohn Snow 48792834894SJohn Snow # Python 3.7 with setuptools installed. 48892834894SJohn Snow try: 48992834894SJohn Snow return _gen_pkg_resources(packages) 49092834894SJohn Snow except ImportError as exc: 49192834894SJohn Snow logger.debug("%s", str(exc)) 49292834894SJohn Snow raise Ouch( 49392834894SJohn Snow "Neither importlib.metadata nor pkg_resources found, " 49492834894SJohn Snow "can't generate console script shims.\n" 49592834894SJohn Snow "Use Python 3.8+, or install importlib-metadata or setuptools." 49692834894SJohn Snow ) from exc 49792834894SJohn Snow 49892834894SJohn Snow maker = distlib.scripts.ScriptMaker(None, bin_path) 49992834894SJohn Snow maker.variants = {""} 50092834894SJohn Snow maker.clobber = False 50192834894SJohn Snow 50292834894SJohn Snow for entry_point in _get_entry_points(): 50392834894SJohn Snow for filename in maker.make(entry_point): 50492834894SJohn Snow logger.debug("wrote console_script '%s'", filename) 50592834894SJohn Snow 50692834894SJohn Snow 507c8049626SJohn Snowdef checkpip() -> bool: 508c8049626SJohn Snow """ 509c8049626SJohn Snow Debian10 has a pip that's broken when used inside of a virtual environment. 510c8049626SJohn Snow 511c8049626SJohn Snow We try to detect and correct that case here. 512c8049626SJohn Snow """ 513c8049626SJohn Snow try: 514c8049626SJohn Snow # pylint: disable=import-outside-toplevel,unused-import,import-error 515c8049626SJohn Snow # pylint: disable=redefined-outer-name 516c8049626SJohn Snow import pip._internal # type: ignore # noqa: F401 517c8049626SJohn Snow 518c8049626SJohn Snow logger.debug("pip appears to be working correctly.") 519c8049626SJohn Snow return False 520c8049626SJohn Snow except ModuleNotFoundError as exc: 521c8049626SJohn Snow if exc.name == "pip._internal": 522c8049626SJohn Snow # Uh, fair enough. They did say "internal". 523c8049626SJohn Snow # Let's just assume it's fine. 524c8049626SJohn Snow return False 525c8049626SJohn Snow logger.warning("pip appears to be malfunctioning: %s", str(exc)) 526c8049626SJohn Snow 527c8049626SJohn Snow check_ensurepip("pip appears to be non-functional, and ") 528c8049626SJohn Snow 529c8049626SJohn Snow logger.debug("Attempting to repair pip ...") 530c8049626SJohn Snow subprocess.run( 531c8049626SJohn Snow (sys.executable, "-m", "ensurepip"), 532c8049626SJohn Snow stdout=subprocess.DEVNULL, 533c8049626SJohn Snow check=True, 534c8049626SJohn Snow ) 535c8049626SJohn Snow logger.debug("Pip is now (hopefully) repaired!") 536c8049626SJohn Snow return True 537c8049626SJohn Snow 538c8049626SJohn Snow 5394695a22eSJohn Snowdef pkgname_from_depspec(dep_spec: str) -> str: 5404695a22eSJohn Snow """ 5414695a22eSJohn Snow Parse package name out of a PEP-508 depspec. 5424695a22eSJohn Snow 5434695a22eSJohn Snow See https://peps.python.org/pep-0508/#names 5444695a22eSJohn Snow """ 5454695a22eSJohn Snow match = re.match( 5464695a22eSJohn Snow r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE 5474695a22eSJohn Snow ) 5484695a22eSJohn Snow if not match: 5494695a22eSJohn Snow raise ValueError( 5504695a22eSJohn Snow f"dep_spec '{dep_spec}'" 5514695a22eSJohn Snow " does not appear to contain a valid package name" 5524695a22eSJohn Snow ) 5534695a22eSJohn Snow return match.group(0) 5544695a22eSJohn Snow 5554695a22eSJohn Snow 55647a90a51SPaolo Bonzinidef _get_path_importlib(package: str) -> Optional[str]: 55747a90a51SPaolo Bonzini # pylint: disable=import-outside-toplevel 55847a90a51SPaolo Bonzini # pylint: disable=no-name-in-module 55947a90a51SPaolo Bonzini # pylint: disable=import-error 56047a90a51SPaolo Bonzini try: 56147a90a51SPaolo Bonzini # First preference: Python 3.8+ stdlib 56247a90a51SPaolo Bonzini from importlib.metadata import ( # type: ignore 56347a90a51SPaolo Bonzini PackageNotFoundError, 56447a90a51SPaolo Bonzini distribution, 56547a90a51SPaolo Bonzini ) 56647a90a51SPaolo Bonzini except ImportError as exc: 56747a90a51SPaolo Bonzini logger.debug("%s", str(exc)) 56847a90a51SPaolo Bonzini # Second preference: Commonly available PyPI backport 56947a90a51SPaolo Bonzini from importlib_metadata import ( # type: ignore 57047a90a51SPaolo Bonzini PackageNotFoundError, 57147a90a51SPaolo Bonzini distribution, 57247a90a51SPaolo Bonzini ) 57347a90a51SPaolo Bonzini 57447a90a51SPaolo Bonzini try: 57547a90a51SPaolo Bonzini return str(distribution(package).locate_file(".")) 57647a90a51SPaolo Bonzini except PackageNotFoundError: 57747a90a51SPaolo Bonzini return None 57847a90a51SPaolo Bonzini 57947a90a51SPaolo Bonzini 58047a90a51SPaolo Bonzinidef _get_path_pkg_resources(package: str) -> Optional[str]: 58147a90a51SPaolo Bonzini # pylint: disable=import-outside-toplevel 58247a90a51SPaolo Bonzini # Bundled with setuptools; has a good chance of being available. 58347a90a51SPaolo Bonzini import pkg_resources 58447a90a51SPaolo Bonzini 58547a90a51SPaolo Bonzini try: 58647a90a51SPaolo Bonzini return str(pkg_resources.get_distribution(package).location) 58747a90a51SPaolo Bonzini except pkg_resources.DistributionNotFound: 58847a90a51SPaolo Bonzini return None 58947a90a51SPaolo Bonzini 59047a90a51SPaolo Bonzini 59147a90a51SPaolo Bonzinidef _get_path(package: str) -> Optional[str]: 59247a90a51SPaolo Bonzini try: 59347a90a51SPaolo Bonzini return _get_path_importlib(package) 59447a90a51SPaolo Bonzini except ImportError as exc: 59547a90a51SPaolo Bonzini logger.debug("%s", str(exc)) 59647a90a51SPaolo Bonzini 59747a90a51SPaolo Bonzini try: 59847a90a51SPaolo Bonzini return _get_path_pkg_resources(package) 59947a90a51SPaolo Bonzini except ImportError as exc: 60047a90a51SPaolo Bonzini logger.debug("%s", str(exc)) 60147a90a51SPaolo Bonzini raise Ouch( 60247a90a51SPaolo Bonzini "Neither importlib.metadata nor pkg_resources found. " 60347a90a51SPaolo Bonzini "Use Python 3.8+, or install importlib-metadata or setuptools." 60447a90a51SPaolo Bonzini ) from exc 60547a90a51SPaolo Bonzini 60647a90a51SPaolo Bonzini 60747a90a51SPaolo Bonzinidef _path_is_prefix(prefix: Optional[str], path: str) -> bool: 60847a90a51SPaolo Bonzini try: 60947a90a51SPaolo Bonzini return ( 61047a90a51SPaolo Bonzini prefix is not None and os.path.commonpath([prefix, path]) == prefix 61147a90a51SPaolo Bonzini ) 61247a90a51SPaolo Bonzini except ValueError: 61347a90a51SPaolo Bonzini return False 61447a90a51SPaolo Bonzini 61547a90a51SPaolo Bonzini 61647a90a51SPaolo Bonzinidef _is_system_package(package: str) -> bool: 61747a90a51SPaolo Bonzini path = _get_path(package) 61847a90a51SPaolo Bonzini return path is not None and not ( 61947a90a51SPaolo Bonzini _path_is_prefix(sysconfig.get_path("purelib"), path) 62047a90a51SPaolo Bonzini or _path_is_prefix(sysconfig.get_path("platlib"), path) 62147a90a51SPaolo Bonzini ) 62247a90a51SPaolo Bonzini 62347a90a51SPaolo Bonzini 624c673f3d0SPaolo Bonzinidef _get_version_importlib(package: str) -> Optional[str]: 625c673f3d0SPaolo Bonzini # pylint: disable=import-outside-toplevel 626c673f3d0SPaolo Bonzini # pylint: disable=no-name-in-module 627c673f3d0SPaolo Bonzini # pylint: disable=import-error 628c673f3d0SPaolo Bonzini try: 629c673f3d0SPaolo Bonzini # First preference: Python 3.8+ stdlib 630c673f3d0SPaolo Bonzini from importlib.metadata import ( # type: ignore 631c673f3d0SPaolo Bonzini PackageNotFoundError, 632c673f3d0SPaolo Bonzini distribution, 633c673f3d0SPaolo Bonzini ) 634c673f3d0SPaolo Bonzini except ImportError as exc: 635c673f3d0SPaolo Bonzini logger.debug("%s", str(exc)) 636c673f3d0SPaolo Bonzini # Second preference: Commonly available PyPI backport 637c673f3d0SPaolo Bonzini from importlib_metadata import ( # type: ignore 638c673f3d0SPaolo Bonzini PackageNotFoundError, 639c673f3d0SPaolo Bonzini distribution, 640c673f3d0SPaolo Bonzini ) 641c673f3d0SPaolo Bonzini 642c673f3d0SPaolo Bonzini try: 643c673f3d0SPaolo Bonzini return str(distribution(package).version) 644c673f3d0SPaolo Bonzini except PackageNotFoundError: 645c673f3d0SPaolo Bonzini return None 646c673f3d0SPaolo Bonzini 647c673f3d0SPaolo Bonzini 648c673f3d0SPaolo Bonzinidef _get_version_pkg_resources(package: str) -> Optional[str]: 649c673f3d0SPaolo Bonzini # pylint: disable=import-outside-toplevel 650c673f3d0SPaolo Bonzini # Bundled with setuptools; has a good chance of being available. 651c673f3d0SPaolo Bonzini import pkg_resources 652c673f3d0SPaolo Bonzini 653c673f3d0SPaolo Bonzini try: 654c673f3d0SPaolo Bonzini return str(pkg_resources.get_distribution(package).version) 655c673f3d0SPaolo Bonzini except pkg_resources.DistributionNotFound: 656c673f3d0SPaolo Bonzini return None 657c673f3d0SPaolo Bonzini 658c673f3d0SPaolo Bonzini 659c673f3d0SPaolo Bonzinidef _get_version(package: str) -> Optional[str]: 660c673f3d0SPaolo Bonzini try: 661c673f3d0SPaolo Bonzini return _get_version_importlib(package) 662c673f3d0SPaolo Bonzini except ImportError as exc: 663c673f3d0SPaolo Bonzini logger.debug("%s", str(exc)) 664c673f3d0SPaolo Bonzini 665c673f3d0SPaolo Bonzini try: 666c673f3d0SPaolo Bonzini return _get_version_pkg_resources(package) 667c673f3d0SPaolo Bonzini except ImportError as exc: 668c673f3d0SPaolo Bonzini logger.debug("%s", str(exc)) 669c673f3d0SPaolo Bonzini raise Ouch( 670c673f3d0SPaolo Bonzini "Neither importlib.metadata nor pkg_resources found. " 671c673f3d0SPaolo Bonzini "Use Python 3.8+, or install importlib-metadata or setuptools." 672c673f3d0SPaolo Bonzini ) from exc 673c673f3d0SPaolo Bonzini 674c673f3d0SPaolo Bonzini 6754695a22eSJohn Snowdef diagnose( 6764695a22eSJohn Snow dep_spec: str, 6774695a22eSJohn Snow online: bool, 6784695a22eSJohn Snow wheels_dir: Optional[Union[str, Path]], 6794695a22eSJohn Snow prog: Optional[str], 6804695a22eSJohn Snow) -> Tuple[str, bool]: 6814695a22eSJohn Snow """ 6824695a22eSJohn Snow Offer a summary to the user as to why a package failed to be installed. 6834695a22eSJohn Snow 6844695a22eSJohn Snow :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5' 6854695a22eSJohn Snow :param online: Did we allow PyPI access? 6864695a22eSJohn Snow :param prog: 6874695a22eSJohn Snow Optionally, a shell program name that can be used as a 6884695a22eSJohn Snow bellwether to detect if this program is installed elsewhere on 6894695a22eSJohn Snow the system. This is used to offer advice when a program is 6904695a22eSJohn Snow detected for a different python version. 6914695a22eSJohn Snow :param wheels_dir: 6924695a22eSJohn Snow Optionally, a directory that was searched for vendored packages. 6934695a22eSJohn Snow """ 6944695a22eSJohn Snow # pylint: disable=too-many-branches 6954695a22eSJohn Snow 6964695a22eSJohn Snow # Some errors are not particularly serious 6974695a22eSJohn Snow bad = False 6984695a22eSJohn Snow 6994695a22eSJohn Snow pkg_name = pkgname_from_depspec(dep_spec) 700c673f3d0SPaolo Bonzini pkg_version = _get_version(pkg_name) 7014695a22eSJohn Snow 7024695a22eSJohn Snow lines = [] 7034695a22eSJohn Snow 7044695a22eSJohn Snow if pkg_version: 7054695a22eSJohn Snow lines.append( 7064695a22eSJohn Snow f"Python package '{pkg_name}' version '{pkg_version}' was found," 7074695a22eSJohn Snow " but isn't suitable." 7084695a22eSJohn Snow ) 7094695a22eSJohn Snow else: 7104695a22eSJohn Snow lines.append( 711c673f3d0SPaolo Bonzini f"Python package '{pkg_name}' was not found nor installed." 7124695a22eSJohn Snow ) 7134695a22eSJohn Snow 7144695a22eSJohn Snow if wheels_dir: 7154695a22eSJohn Snow lines.append( 7164695a22eSJohn Snow "No suitable version found in, or failed to install from" 7174695a22eSJohn Snow f" '{wheels_dir}'." 7184695a22eSJohn Snow ) 7194695a22eSJohn Snow bad = True 7204695a22eSJohn Snow 7214695a22eSJohn Snow if online: 7224695a22eSJohn Snow lines.append("A suitable version could not be obtained from PyPI.") 7234695a22eSJohn Snow bad = True 7244695a22eSJohn Snow else: 7254695a22eSJohn Snow lines.append( 7264695a22eSJohn Snow "mkvenv was configured to operate offline and did not check PyPI." 7274695a22eSJohn Snow ) 7284695a22eSJohn Snow 7294695a22eSJohn Snow if prog and not pkg_version: 7304695a22eSJohn Snow which = shutil.which(prog) 7314695a22eSJohn Snow if which: 7324695a22eSJohn Snow if sys.base_prefix in site.PREFIXES: 7334695a22eSJohn Snow pypath = Path(sys.executable).resolve() 7344695a22eSJohn Snow lines.append( 7354695a22eSJohn Snow f"'{prog}' was detected on your system at '{which}', " 7364695a22eSJohn Snow f"but the Python package '{pkg_name}' was not found by " 7374695a22eSJohn Snow f"this Python interpreter ('{pypath}'). " 7384695a22eSJohn Snow f"Typically this means that '{prog}' has been installed " 7394695a22eSJohn Snow "against a different Python interpreter on your system." 7404695a22eSJohn Snow ) 7414695a22eSJohn Snow else: 7424695a22eSJohn Snow lines.append( 7434695a22eSJohn Snow f"'{prog}' was detected on your system at '{which}', " 7444695a22eSJohn Snow "but the build is using an isolated virtual environment." 7454695a22eSJohn Snow ) 7464695a22eSJohn Snow bad = True 7474695a22eSJohn Snow 7484695a22eSJohn Snow lines = [f" • {line}" for line in lines] 7494695a22eSJohn Snow if bad: 7504695a22eSJohn Snow lines.insert(0, f"Could not provide build dependency '{dep_spec}':") 7514695a22eSJohn Snow else: 7524695a22eSJohn Snow lines.insert(0, f"'{dep_spec}' not found:") 7534695a22eSJohn Snow return os.linesep.join(lines), bad 7544695a22eSJohn Snow 7554695a22eSJohn Snow 756c5538eedSJohn Snowdef pip_install( 757c5538eedSJohn Snow args: Sequence[str], 758c5538eedSJohn Snow online: bool = False, 759c5538eedSJohn Snow wheels_dir: Optional[Union[str, Path]] = None, 760c5538eedSJohn Snow) -> None: 761c5538eedSJohn Snow """ 762c5538eedSJohn Snow Use pip to install a package or package(s) as specified in @args. 763c5538eedSJohn Snow """ 764c5538eedSJohn Snow loud = bool( 765c5538eedSJohn Snow os.environ.get("DEBUG") 766c5538eedSJohn Snow or os.environ.get("GITLAB_CI") 767c5538eedSJohn Snow or os.environ.get("V") 768c5538eedSJohn Snow ) 769c5538eedSJohn Snow 770c5538eedSJohn Snow full_args = [ 771c5538eedSJohn Snow sys.executable, 772c5538eedSJohn Snow "-m", 773c5538eedSJohn Snow "pip", 774c5538eedSJohn Snow "install", 775c5538eedSJohn Snow "--disable-pip-version-check", 776c5538eedSJohn Snow "-v" if loud else "-q", 777c5538eedSJohn Snow ] 778c5538eedSJohn Snow if not online: 779c5538eedSJohn Snow full_args += ["--no-index"] 780c5538eedSJohn Snow if wheels_dir: 781c5538eedSJohn Snow full_args += ["--find-links", f"file://{str(wheels_dir)}"] 782c5538eedSJohn Snow full_args += list(args) 783c5538eedSJohn Snow subprocess.run( 784c5538eedSJohn Snow full_args, 785c5538eedSJohn Snow check=True, 786c5538eedSJohn Snow ) 787c5538eedSJohn Snow 788c5538eedSJohn Snow 7894695a22eSJohn Snowdef _do_ensure( 790c5538eedSJohn Snow dep_specs: Sequence[str], 791c5538eedSJohn Snow online: bool = False, 792c5538eedSJohn Snow wheels_dir: Optional[Union[str, Path]] = None, 793d37c21b5SPaolo Bonzini prog: Optional[str] = None, 794d37c21b5SPaolo Bonzini) -> Optional[Tuple[str, bool]]: 795c5538eedSJohn Snow """ 796c5538eedSJohn Snow Use pip to ensure we have the package specified by @dep_specs. 797c5538eedSJohn Snow 798c5538eedSJohn Snow If the package is already installed, do nothing. If online and 799c5538eedSJohn Snow wheels_dir are both provided, prefer packages found in wheels_dir 800c5538eedSJohn Snow first before connecting to PyPI. 801c5538eedSJohn Snow 802c5538eedSJohn Snow :param dep_specs: 803c5538eedSJohn Snow PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. 804c5538eedSJohn Snow :param online: If True, fall back to PyPI. 805c5538eedSJohn Snow :param wheels_dir: If specified, search this path for packages. 806c5538eedSJohn Snow """ 807c5538eedSJohn Snow absent = [] 80892834894SJohn Snow present = [] 809*67b9a83dSPaolo Bonzini canary = None 810c5538eedSJohn Snow for spec in dep_specs: 811c5538eedSJohn Snow matcher = distlib.version.LegacyMatcher(spec) 812c673f3d0SPaolo Bonzini ver = _get_version(matcher.name) 81347a90a51SPaolo Bonzini if ( 81447a90a51SPaolo Bonzini ver is None 81547a90a51SPaolo Bonzini # Always pass installed package to pip, so that they can be 81647a90a51SPaolo Bonzini # updated if the requested version changes 81747a90a51SPaolo Bonzini or not _is_system_package(matcher.name) 81847a90a51SPaolo Bonzini or not matcher.match(distlib.version.LegacyVersion(ver)) 819c673f3d0SPaolo Bonzini ): 820c5538eedSJohn Snow absent.append(spec) 821*67b9a83dSPaolo Bonzini if spec == dep_specs[0]: 822*67b9a83dSPaolo Bonzini canary = prog 823c5538eedSJohn Snow else: 824c673f3d0SPaolo Bonzini logger.info("found %s %s", matcher.name, ver) 82592834894SJohn Snow present.append(matcher.name) 82692834894SJohn Snow 82792834894SJohn Snow if present: 82892834894SJohn Snow generate_console_scripts(present) 829c5538eedSJohn Snow 830c5538eedSJohn Snow if absent: 831d37c21b5SPaolo Bonzini if online or wheels_dir: 832c5538eedSJohn Snow # Some packages are missing or aren't a suitable version, 833c5538eedSJohn Snow # install a suitable (possibly vendored) package. 834c5538eedSJohn Snow print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr) 835d37c21b5SPaolo Bonzini try: 836c5538eedSJohn Snow pip_install(args=absent, online=online, wheels_dir=wheels_dir) 837d37c21b5SPaolo Bonzini return None 838d37c21b5SPaolo Bonzini except subprocess.CalledProcessError: 839d37c21b5SPaolo Bonzini pass 840d37c21b5SPaolo Bonzini 841d37c21b5SPaolo Bonzini return diagnose( 842d37c21b5SPaolo Bonzini absent[0], 843d37c21b5SPaolo Bonzini online, 844d37c21b5SPaolo Bonzini wheels_dir, 845*67b9a83dSPaolo Bonzini canary, 846d37c21b5SPaolo Bonzini ) 847d37c21b5SPaolo Bonzini 848d37c21b5SPaolo Bonzini return None 849c5538eedSJohn Snow 850c5538eedSJohn Snow 8514695a22eSJohn Snowdef ensure( 8524695a22eSJohn Snow dep_specs: Sequence[str], 8534695a22eSJohn Snow online: bool = False, 8544695a22eSJohn Snow wheels_dir: Optional[Union[str, Path]] = None, 8554695a22eSJohn Snow prog: Optional[str] = None, 8564695a22eSJohn Snow) -> None: 8574695a22eSJohn Snow """ 8584695a22eSJohn Snow Use pip to ensure we have the package specified by @dep_specs. 8594695a22eSJohn Snow 8604695a22eSJohn Snow If the package is already installed, do nothing. If online and 8614695a22eSJohn Snow wheels_dir are both provided, prefer packages found in wheels_dir 8624695a22eSJohn Snow first before connecting to PyPI. 8634695a22eSJohn Snow 8644695a22eSJohn Snow :param dep_specs: 8654695a22eSJohn Snow PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. 8664695a22eSJohn Snow :param online: If True, fall back to PyPI. 8674695a22eSJohn Snow :param wheels_dir: If specified, search this path for packages. 8684695a22eSJohn Snow :param prog: 8694695a22eSJohn Snow If specified, use this program name for error diagnostics that will 8704695a22eSJohn Snow be presented to the user. e.g., 'sphinx-build' can be used as a 8714695a22eSJohn Snow bellwether for the presence of 'sphinx'. 8724695a22eSJohn Snow """ 8734695a22eSJohn Snow print(f"mkvenv: checking for {', '.join(dep_specs)}", file=sys.stderr) 87468ea6d17SJohn Snow 87568ea6d17SJohn Snow if not HAVE_DISTLIB: 87668ea6d17SJohn Snow raise Ouch("a usable distlib could not be found, please install it") 87768ea6d17SJohn Snow 878d37c21b5SPaolo Bonzini result = _do_ensure(dep_specs, online, wheels_dir, prog) 879d37c21b5SPaolo Bonzini if result: 8804695a22eSJohn Snow # Well, that's not good. 881d37c21b5SPaolo Bonzini if result[1]: 882d37c21b5SPaolo Bonzini raise Ouch(result[0]) 883d37c21b5SPaolo Bonzini raise SystemExit(f"\n{result[0]}\n\n") 8844695a22eSJohn Snow 8854695a22eSJohn Snow 886f1ad527fSJohn Snowdef post_venv_setup() -> None: 887f1ad527fSJohn Snow """ 888f1ad527fSJohn Snow This is intended to be run *inside the venv* after it is created. 889f1ad527fSJohn Snow """ 890f1ad527fSJohn Snow logger.debug("post_venv_setup()") 891c8049626SJohn Snow # Test for a broken pip (Debian 10 or derivative?) and fix it if needed 892c8049626SJohn Snow if not checkpip(): 893c8049626SJohn Snow # Finally, generate a 'pip' script so the venv is usable in a normal 894f1ad527fSJohn Snow # way from the CLI. This only happens when we inherited pip from a 895f1ad527fSJohn Snow # parent/system-site and haven't run ensurepip in some way. 896f1ad527fSJohn Snow generate_console_scripts(["pip"]) 897f1ad527fSJohn Snow 898f1ad527fSJohn Snow 899dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None: 900dd84028fSJohn Snow subparser = subparsers.add_parser("create", help="create a venv") 901dd84028fSJohn Snow subparser.add_argument( 902dd84028fSJohn Snow "target", 903dd84028fSJohn Snow type=str, 904dd84028fSJohn Snow action="store", 905dd84028fSJohn Snow help="Target directory to install virtual environment into.", 906dd84028fSJohn Snow ) 907dd84028fSJohn Snow 908dd84028fSJohn Snow 909f1ad527fSJohn Snowdef _add_post_init_subcommand(subparsers: Any) -> None: 910f1ad527fSJohn Snow subparsers.add_parser("post_init", help="post-venv initialization") 911f1ad527fSJohn Snow 912f1ad527fSJohn Snow 913c5538eedSJohn Snowdef _add_ensure_subcommand(subparsers: Any) -> None: 914c5538eedSJohn Snow subparser = subparsers.add_parser( 915c5538eedSJohn Snow "ensure", help="Ensure that the specified package is installed." 916c5538eedSJohn Snow ) 917c5538eedSJohn Snow subparser.add_argument( 918c5538eedSJohn Snow "--online", 919c5538eedSJohn Snow action="store_true", 920c5538eedSJohn Snow help="Install packages from PyPI, if necessary.", 921c5538eedSJohn Snow ) 922c5538eedSJohn Snow subparser.add_argument( 923c5538eedSJohn Snow "--dir", 924c5538eedSJohn Snow type=str, 925c5538eedSJohn Snow action="store", 926c5538eedSJohn Snow help="Path to vendored packages where we may install from.", 927c5538eedSJohn Snow ) 928c5538eedSJohn Snow subparser.add_argument( 9294695a22eSJohn Snow "--diagnose", 9304695a22eSJohn Snow type=str, 9314695a22eSJohn Snow action="store", 9324695a22eSJohn Snow help=( 9334695a22eSJohn Snow "Name of a shell utility to use for " 9344695a22eSJohn Snow "diagnostics if this command fails." 9354695a22eSJohn Snow ), 9364695a22eSJohn Snow ) 9374695a22eSJohn Snow subparser.add_argument( 938c5538eedSJohn Snow "dep_specs", 939c5538eedSJohn Snow type=str, 940c5538eedSJohn Snow action="store", 941c5538eedSJohn Snow help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'", 942c5538eedSJohn Snow nargs="+", 943c5538eedSJohn Snow ) 944c5538eedSJohn Snow 945c5538eedSJohn Snow 946dd84028fSJohn Snowdef main() -> int: 947dd84028fSJohn Snow """CLI interface to make_qemu_venv. See module docstring.""" 948dd84028fSJohn Snow if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"): 949dd84028fSJohn Snow # You're welcome. 950dd84028fSJohn Snow logging.basicConfig(level=logging.DEBUG) 951c5538eedSJohn Snow else: 952c5538eedSJohn Snow if os.environ.get("V"): 953dd84028fSJohn Snow logging.basicConfig(level=logging.INFO) 954dd84028fSJohn Snow 955dd84028fSJohn Snow parser = argparse.ArgumentParser( 956dd84028fSJohn Snow prog="mkvenv", 957dd84028fSJohn Snow description="QEMU pyvenv bootstrapping utility", 958dd84028fSJohn Snow ) 959dd84028fSJohn Snow subparsers = parser.add_subparsers( 960dd84028fSJohn Snow title="Commands", 961dd84028fSJohn Snow dest="command", 96202312f1aSPaolo Bonzini required=True, 963dd84028fSJohn Snow metavar="command", 964dd84028fSJohn Snow help="Description", 965dd84028fSJohn Snow ) 966dd84028fSJohn Snow 967dd84028fSJohn Snow _add_create_subcommand(subparsers) 968f1ad527fSJohn Snow _add_post_init_subcommand(subparsers) 969c5538eedSJohn Snow _add_ensure_subcommand(subparsers) 970dd84028fSJohn Snow 971dd84028fSJohn Snow args = parser.parse_args() 972dd84028fSJohn Snow try: 973dd84028fSJohn Snow if args.command == "create": 974dd84028fSJohn Snow make_venv( 975dd84028fSJohn Snow args.target, 976dd84028fSJohn Snow system_site_packages=True, 977dd84028fSJohn Snow clear=True, 978dd84028fSJohn Snow ) 979f1ad527fSJohn Snow if args.command == "post_init": 980f1ad527fSJohn Snow post_venv_setup() 981c5538eedSJohn Snow if args.command == "ensure": 982c5538eedSJohn Snow ensure( 983c5538eedSJohn Snow dep_specs=args.dep_specs, 984c5538eedSJohn Snow online=args.online, 985c5538eedSJohn Snow wheels_dir=args.dir, 9864695a22eSJohn Snow prog=args.diagnose, 987c5538eedSJohn Snow ) 988dd84028fSJohn Snow logger.debug("mkvenv.py %s: exiting", args.command) 989dd84028fSJohn Snow except Ouch as exc: 990dd84028fSJohn Snow print("\n*** Ouch! ***\n", file=sys.stderr) 991dd84028fSJohn Snow print(str(exc), "\n\n", file=sys.stderr) 992dd84028fSJohn Snow return 1 993dd84028fSJohn Snow except SystemExit: 994dd84028fSJohn Snow raise 995dd84028fSJohn Snow except: # pylint: disable=bare-except 996dd84028fSJohn Snow logger.exception("mkvenv did not complete successfully:") 997dd84028fSJohn Snow return 2 998dd84028fSJohn Snow return 0 999dd84028fSJohn Snow 1000dd84028fSJohn Snow 1001dd84028fSJohn Snowif __name__ == "__main__": 1002dd84028fSJohn Snow sys.exit(main()) 1003