1""" 2mkvenv - QEMU pyvenv bootstrapping utility 3 4usage: mkvenv [-h] command ... 5 6QEMU pyvenv bootstrapping utility 7 8options: 9 -h, --help show this help message and exit 10 11Commands: 12 command Description 13 create create a venv 14 post_init 15 post-venv initialization 16 ensure Ensure that the specified package is installed. 17 18-------------------------------------------------- 19 20usage: mkvenv create [-h] target 21 22positional arguments: 23 target Target directory to install virtual environment into. 24 25options: 26 -h, --help show this help message and exit 27 28-------------------------------------------------- 29 30usage: mkvenv post_init [-h] 31 32options: 33 -h, --help show this help message and exit 34 35-------------------------------------------------- 36 37usage: mkvenv ensure [-h] [--online] [--dir DIR] dep_spec... 38 39positional arguments: 40 dep_spec PEP 508 Dependency specification, e.g. 'meson>=0.61.5' 41 42options: 43 -h, --help show this help message and exit 44 --online Install packages from PyPI, if necessary. 45 --dir DIR Path to vendored packages where we may install from. 46 47""" 48 49# Copyright (C) 2022-2023 Red Hat, Inc. 50# 51# Authors: 52# John Snow <jsnow@redhat.com> 53# Paolo Bonzini <pbonzini@redhat.com> 54# 55# This work is licensed under the terms of the GNU GPL, version 2 or 56# later. See the COPYING file in the top-level directory. 57 58import argparse 59from importlib.util import find_spec 60import logging 61import os 62from pathlib import Path 63import re 64import shutil 65import site 66import subprocess 67import sys 68import sysconfig 69from types import SimpleNamespace 70from typing import ( 71 Any, 72 Iterator, 73 Optional, 74 Sequence, 75 Tuple, 76 Union, 77) 78import venv 79import warnings 80 81 82# Try to load distlib, with a fallback to pip's vendored version. 83# HAVE_DISTLIB is checked below, just-in-time, so that mkvenv does not fail 84# outside the venv or before a potential call to ensurepip in checkpip(). 85HAVE_DISTLIB = True 86try: 87 import distlib.database 88 import distlib.scripts 89 import distlib.version 90except ImportError: 91 try: 92 # Reach into pip's cookie jar. pylint and flake8 don't understand 93 # that these imports will be used via distlib.xxx. 94 from pip._vendor import distlib 95 import pip._vendor.distlib.database # noqa, pylint: disable=unused-import 96 import pip._vendor.distlib.scripts # noqa, pylint: disable=unused-import 97 import pip._vendor.distlib.version # noqa, pylint: disable=unused-import 98 except ImportError: 99 HAVE_DISTLIB = False 100 101# Do not add any mandatory dependencies from outside the stdlib: 102# This script *must* be usable standalone! 103 104DirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] 105logger = logging.getLogger("mkvenv") 106 107 108def inside_a_venv() -> bool: 109 """Returns True if it is executed inside of a virtual environment.""" 110 return sys.prefix != sys.base_prefix 111 112 113class Ouch(RuntimeError): 114 """An Exception class we can't confuse with a builtin.""" 115 116 117class QemuEnvBuilder(venv.EnvBuilder): 118 """ 119 An extension of venv.EnvBuilder for building QEMU's configure-time venv. 120 121 The primary difference is that it emulates a "nested" virtual 122 environment when invoked from inside of an existing virtual 123 environment by including packages from the parent. Also, 124 "ensurepip" is replaced if possible with just recreating pip's 125 console_scripts inside the virtual environment. 126 127 Parameters for base class init: 128 - system_site_packages: bool = False 129 - clear: bool = False 130 - symlinks: bool = False 131 - upgrade: bool = False 132 - with_pip: bool = False 133 - prompt: Optional[str] = None 134 - upgrade_deps: bool = False (Since 3.9) 135 """ 136 137 def __init__(self, *args: Any, **kwargs: Any) -> None: 138 logger.debug("QemuEnvBuilder.__init__(...)") 139 140 # For nested venv emulation: 141 self.use_parent_packages = False 142 if inside_a_venv(): 143 # Include parent packages only if we're in a venv and 144 # system_site_packages was True. 145 self.use_parent_packages = kwargs.pop( 146 "system_site_packages", False 147 ) 148 # Include system_site_packages only when the parent, 149 # The venv we are currently in, also does so. 150 kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES 151 152 # ensurepip is slow: venv creation can be very fast for cases where 153 # we allow the use of system_site_packages. Therefore, ensurepip is 154 # replaced with our own script generation once the virtual environment 155 # is setup. 156 self.want_pip = kwargs.get("with_pip", False) 157 if self.want_pip: 158 if ( 159 kwargs.get("system_site_packages", False) 160 and not need_ensurepip() 161 ): 162 kwargs["with_pip"] = False 163 else: 164 check_ensurepip(suggest_remedy=True) 165 166 super().__init__(*args, **kwargs) 167 168 # Make the context available post-creation: 169 self._context: Optional[SimpleNamespace] = None 170 171 def get_parent_libpath(self) -> Optional[str]: 172 """Return the libpath of the parent venv, if applicable.""" 173 if self.use_parent_packages: 174 return sysconfig.get_path("purelib") 175 return None 176 177 @staticmethod 178 def compute_venv_libpath(context: SimpleNamespace) -> str: 179 """ 180 Compatibility wrapper for context.lib_path for Python < 3.12 181 """ 182 # Python 3.12+, not strictly necessary because it's documented 183 # to be the same as 3.10 code below: 184 if sys.version_info >= (3, 12): 185 return context.lib_path 186 187 # Python 3.10+ 188 if "venv" in sysconfig.get_scheme_names(): 189 lib_path = sysconfig.get_path( 190 "purelib", scheme="venv", vars={"base": context.env_dir} 191 ) 192 assert lib_path is not None 193 return lib_path 194 195 # For Python <= 3.9 we need to hardcode this. Fortunately the 196 # code below was the same in Python 3.6-3.10, so there is only 197 # one case. 198 if sys.platform == "win32": 199 return os.path.join(context.env_dir, "Lib", "site-packages") 200 return os.path.join( 201 context.env_dir, 202 "lib", 203 "python%d.%d" % sys.version_info[:2], 204 "site-packages", 205 ) 206 207 def ensure_directories(self, env_dir: DirType) -> SimpleNamespace: 208 logger.debug("ensure_directories(env_dir=%s)", env_dir) 209 self._context = super().ensure_directories(env_dir) 210 return self._context 211 212 def create(self, env_dir: DirType) -> None: 213 logger.debug("create(env_dir=%s)", env_dir) 214 super().create(env_dir) 215 assert self._context is not None 216 self.post_post_setup(self._context) 217 218 def post_post_setup(self, context: SimpleNamespace) -> None: 219 """ 220 The final, final hook. Enter the venv and run commands inside of it. 221 """ 222 if self.use_parent_packages: 223 # We're inside of a venv and we want to include the parent 224 # venv's packages. 225 parent_libpath = self.get_parent_libpath() 226 assert parent_libpath is not None 227 logger.debug("parent_libpath: %s", parent_libpath) 228 229 our_libpath = self.compute_venv_libpath(context) 230 logger.debug("our_libpath: %s", our_libpath) 231 232 pth_file = os.path.join(our_libpath, "nested.pth") 233 with open(pth_file, "w", encoding="UTF-8") as file: 234 file.write(parent_libpath + os.linesep) 235 236 if self.want_pip: 237 args = [ 238 context.env_exe, 239 __file__, 240 "post_init", 241 ] 242 subprocess.run(args, check=True) 243 244 def get_value(self, field: str) -> str: 245 """ 246 Get a string value from the context namespace after a call to build. 247 248 For valid field names, see: 249 https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories 250 """ 251 ret = getattr(self._context, field) 252 assert isinstance(ret, str) 253 return ret 254 255 256def need_ensurepip() -> bool: 257 """ 258 Tests for the presence of setuptools and pip. 259 260 :return: `True` if we do not detect both packages. 261 """ 262 # Don't try to actually import them, it's fraught with danger: 263 # https://github.com/pypa/setuptools/issues/2993 264 if find_spec("setuptools") and find_spec("pip"): 265 return False 266 return True 267 268 269def check_ensurepip(prefix: str = "", suggest_remedy: bool = False) -> None: 270 """ 271 Check that we have ensurepip. 272 273 Raise a fatal exception with a helpful hint if it isn't available. 274 """ 275 if not find_spec("ensurepip"): 276 msg = ( 277 "Python's ensurepip module is not found.\n" 278 "It's normally part of the Python standard library, " 279 "maybe your distribution packages it separately?\n" 280 "(Debian puts ensurepip in its python3-venv package.)\n" 281 ) 282 if suggest_remedy: 283 msg += ( 284 "Either install ensurepip, or alleviate the need for it in the" 285 " first place by installing pip and setuptools for " 286 f"'{sys.executable}'.\n" 287 ) 288 raise Ouch(prefix + msg) 289 290 # ensurepip uses pyexpat, which can also go missing on us: 291 if not find_spec("pyexpat"): 292 msg = ( 293 "Python's pyexpat module is not found.\n" 294 "It's normally part of the Python standard library, " 295 "maybe your distribution packages it separately?\n" 296 "(NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)\n" 297 ) 298 if suggest_remedy: 299 msg += ( 300 "Either install pyexpat, or alleviate the need for it in the " 301 "first place by installing pip and setuptools for " 302 f"'{sys.executable}'.\n" 303 ) 304 raise Ouch(prefix + msg) 305 306 307def make_venv( # pylint: disable=too-many-arguments 308 env_dir: Union[str, Path], 309 system_site_packages: bool = False, 310 clear: bool = True, 311 symlinks: Optional[bool] = None, 312 with_pip: bool = True, 313) -> None: 314 """ 315 Create a venv using `QemuEnvBuilder`. 316 317 This is analogous to the `venv.create` module-level convenience 318 function that is part of the Python stdblib, except it uses 319 `QemuEnvBuilder` instead. 320 321 :param env_dir: The directory to create/install to. 322 :param system_site_packages: 323 Allow inheriting packages from the system installation. 324 :param clear: When True, fully remove any prior venv and files. 325 :param symlinks: 326 Whether to use symlinks to the target interpreter or not. If 327 left unspecified, it will use symlinks except on Windows to 328 match behavior with the "venv" CLI tool. 329 :param with_pip: 330 Whether to install "pip" binaries or not. 331 """ 332 logger.debug( 333 "%s: make_venv(env_dir=%s, system_site_packages=%s, " 334 "clear=%s, symlinks=%s, with_pip=%s)", 335 __file__, 336 str(env_dir), 337 system_site_packages, 338 clear, 339 symlinks, 340 with_pip, 341 ) 342 343 if symlinks is None: 344 # Default behavior of standard venv CLI 345 symlinks = os.name != "nt" 346 347 builder = QemuEnvBuilder( 348 system_site_packages=system_site_packages, 349 clear=clear, 350 symlinks=symlinks, 351 with_pip=with_pip, 352 ) 353 354 style = "non-isolated" if builder.system_site_packages else "isolated" 355 nested = "" 356 if builder.use_parent_packages: 357 nested = f"(with packages from '{builder.get_parent_libpath()}') " 358 print( 359 f"mkvenv: Creating {style} virtual environment" 360 f" {nested}at '{str(env_dir)}'", 361 file=sys.stderr, 362 ) 363 364 try: 365 logger.debug("Invoking builder.create()") 366 try: 367 builder.create(str(env_dir)) 368 except SystemExit as exc: 369 # Some versions of the venv module raise SystemExit; *nasty*! 370 # We want the exception that prompted it. It might be a subprocess 371 # error that has output we *really* want to see. 372 logger.debug("Intercepted SystemExit from EnvBuilder.create()") 373 raise exc.__cause__ or exc.__context__ or exc 374 logger.debug("builder.create() finished") 375 except subprocess.CalledProcessError as exc: 376 logger.error("mkvenv subprocess failed:") 377 logger.error("cmd: %s", exc.cmd) 378 logger.error("returncode: %d", exc.returncode) 379 380 def _stringify(data: Union[str, bytes]) -> str: 381 if isinstance(data, bytes): 382 return data.decode() 383 return data 384 385 lines = [] 386 if exc.stdout: 387 lines.append("========== stdout ==========") 388 lines.append(_stringify(exc.stdout)) 389 lines.append("============================") 390 if exc.stderr: 391 lines.append("========== stderr ==========") 392 lines.append(_stringify(exc.stderr)) 393 lines.append("============================") 394 if lines: 395 logger.error(os.linesep.join(lines)) 396 397 raise Ouch("VENV creation subprocess failed.") from exc 398 399 # print the python executable to stdout for configure. 400 print(builder.get_value("env_exe")) 401 402 403def _gen_importlib(packages: Sequence[str]) -> Iterator[str]: 404 # pylint: disable=import-outside-toplevel 405 # pylint: disable=no-name-in-module 406 # pylint: disable=import-error 407 try: 408 # First preference: Python 3.8+ stdlib 409 from importlib.metadata import ( # type: ignore 410 PackageNotFoundError, 411 distribution, 412 ) 413 except ImportError as exc: 414 logger.debug("%s", str(exc)) 415 # Second preference: Commonly available PyPI backport 416 from importlib_metadata import ( # type: ignore 417 PackageNotFoundError, 418 distribution, 419 ) 420 421 def _generator() -> Iterator[str]: 422 for package in packages: 423 try: 424 entry_points = distribution(package).entry_points 425 except PackageNotFoundError: 426 continue 427 428 # The EntryPoints type is only available in 3.10+, 429 # treat this as a vanilla list and filter it ourselves. 430 entry_points = filter( 431 lambda ep: ep.group == "console_scripts", entry_points 432 ) 433 434 for entry_point in entry_points: 435 yield f"{entry_point.name} = {entry_point.value}" 436 437 return _generator() 438 439 440def _gen_pkg_resources(packages: Sequence[str]) -> Iterator[str]: 441 # pylint: disable=import-outside-toplevel 442 # Bundled with setuptools; has a good chance of being available. 443 import pkg_resources 444 445 def _generator() -> Iterator[str]: 446 for package in packages: 447 try: 448 eps = pkg_resources.get_entry_map(package, "console_scripts") 449 except pkg_resources.DistributionNotFound: 450 continue 451 452 for entry_point in eps.values(): 453 yield str(entry_point) 454 455 return _generator() 456 457 458def generate_console_scripts( 459 packages: Sequence[str], 460 python_path: Optional[str] = None, 461 bin_path: Optional[str] = None, 462) -> None: 463 """ 464 Generate script shims for console_script entry points in @packages. 465 """ 466 if python_path is None: 467 python_path = sys.executable 468 if bin_path is None: 469 bin_path = sysconfig.get_path("scripts") 470 assert bin_path is not None 471 472 logger.debug( 473 "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)", 474 packages, 475 python_path, 476 bin_path, 477 ) 478 479 if not packages: 480 return 481 482 def _get_entry_points() -> Iterator[str]: 483 """Python 3.7 compatibility shim for iterating entry points.""" 484 # Python 3.8+, or Python 3.7 with importlib_metadata installed. 485 try: 486 return _gen_importlib(packages) 487 except ImportError as exc: 488 logger.debug("%s", str(exc)) 489 490 # Python 3.7 with setuptools installed. 491 try: 492 return _gen_pkg_resources(packages) 493 except ImportError as exc: 494 logger.debug("%s", str(exc)) 495 raise Ouch( 496 "Neither importlib.metadata nor pkg_resources found, " 497 "can't generate console script shims.\n" 498 "Use Python 3.8+, or install importlib-metadata or setuptools." 499 ) from exc 500 501 maker = distlib.scripts.ScriptMaker(None, bin_path) 502 maker.variants = {""} 503 maker.clobber = False 504 505 for entry_point in _get_entry_points(): 506 for filename in maker.make(entry_point): 507 logger.debug("wrote console_script '%s'", filename) 508 509 510def checkpip() -> bool: 511 """ 512 Debian10 has a pip that's broken when used inside of a virtual environment. 513 514 We try to detect and correct that case here. 515 """ 516 try: 517 # pylint: disable=import-outside-toplevel,unused-import,import-error 518 # pylint: disable=redefined-outer-name 519 import pip._internal # type: ignore # noqa: F401 520 521 logger.debug("pip appears to be working correctly.") 522 return False 523 except ModuleNotFoundError as exc: 524 if exc.name == "pip._internal": 525 # Uh, fair enough. They did say "internal". 526 # Let's just assume it's fine. 527 return False 528 logger.warning("pip appears to be malfunctioning: %s", str(exc)) 529 530 check_ensurepip("pip appears to be non-functional, and ") 531 532 logger.debug("Attempting to repair pip ...") 533 subprocess.run( 534 (sys.executable, "-m", "ensurepip"), 535 stdout=subprocess.DEVNULL, 536 check=True, 537 ) 538 logger.debug("Pip is now (hopefully) repaired!") 539 return True 540 541 542def pkgname_from_depspec(dep_spec: str) -> str: 543 """ 544 Parse package name out of a PEP-508 depspec. 545 546 See https://peps.python.org/pep-0508/#names 547 """ 548 match = re.match( 549 r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE 550 ) 551 if not match: 552 raise ValueError( 553 f"dep_spec '{dep_spec}'" 554 " does not appear to contain a valid package name" 555 ) 556 return match.group(0) 557 558 559def diagnose( 560 dep_spec: str, 561 online: bool, 562 wheels_dir: Optional[Union[str, Path]], 563 prog: Optional[str], 564) -> Tuple[str, bool]: 565 """ 566 Offer a summary to the user as to why a package failed to be installed. 567 568 :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5' 569 :param online: Did we allow PyPI access? 570 :param prog: 571 Optionally, a shell program name that can be used as a 572 bellwether to detect if this program is installed elsewhere on 573 the system. This is used to offer advice when a program is 574 detected for a different python version. 575 :param wheels_dir: 576 Optionally, a directory that was searched for vendored packages. 577 """ 578 # pylint: disable=too-many-branches 579 580 # Some errors are not particularly serious 581 bad = False 582 583 pkg_name = pkgname_from_depspec(dep_spec) 584 pkg_version = None 585 586 has_importlib = False 587 try: 588 # Python 3.8+ stdlib 589 # pylint: disable=import-outside-toplevel 590 # pylint: disable=no-name-in-module 591 # pylint: disable=import-error 592 from importlib.metadata import ( # type: ignore 593 PackageNotFoundError, 594 version, 595 ) 596 597 has_importlib = True 598 try: 599 pkg_version = version(pkg_name) 600 except PackageNotFoundError: 601 pass 602 except ModuleNotFoundError: 603 pass 604 605 lines = [] 606 607 if pkg_version: 608 lines.append( 609 f"Python package '{pkg_name}' version '{pkg_version}' was found," 610 " but isn't suitable." 611 ) 612 elif has_importlib: 613 lines.append( 614 f"Python package '{pkg_name}' was not found nor installed." 615 ) 616 else: 617 lines.append( 618 f"Python package '{pkg_name}' is either not found or" 619 " not a suitable version." 620 ) 621 622 if wheels_dir: 623 lines.append( 624 "No suitable version found in, or failed to install from" 625 f" '{wheels_dir}'." 626 ) 627 bad = True 628 629 if online: 630 lines.append("A suitable version could not be obtained from PyPI.") 631 bad = True 632 else: 633 lines.append( 634 "mkvenv was configured to operate offline and did not check PyPI." 635 ) 636 637 if prog and not pkg_version: 638 which = shutil.which(prog) 639 if which: 640 if sys.base_prefix in site.PREFIXES: 641 pypath = Path(sys.executable).resolve() 642 lines.append( 643 f"'{prog}' was detected on your system at '{which}', " 644 f"but the Python package '{pkg_name}' was not found by " 645 f"this Python interpreter ('{pypath}'). " 646 f"Typically this means that '{prog}' has been installed " 647 "against a different Python interpreter on your system." 648 ) 649 else: 650 lines.append( 651 f"'{prog}' was detected on your system at '{which}', " 652 "but the build is using an isolated virtual environment." 653 ) 654 bad = True 655 656 lines = [f" • {line}" for line in lines] 657 if bad: 658 lines.insert(0, f"Could not provide build dependency '{dep_spec}':") 659 else: 660 lines.insert(0, f"'{dep_spec}' not found:") 661 return os.linesep.join(lines), bad 662 663 664def pip_install( 665 args: Sequence[str], 666 online: bool = False, 667 wheels_dir: Optional[Union[str, Path]] = None, 668) -> None: 669 """ 670 Use pip to install a package or package(s) as specified in @args. 671 """ 672 loud = bool( 673 os.environ.get("DEBUG") 674 or os.environ.get("GITLAB_CI") 675 or os.environ.get("V") 676 ) 677 678 full_args = [ 679 sys.executable, 680 "-m", 681 "pip", 682 "install", 683 "--disable-pip-version-check", 684 "-v" if loud else "-q", 685 ] 686 if not online: 687 full_args += ["--no-index"] 688 if wheels_dir: 689 full_args += ["--find-links", f"file://{str(wheels_dir)}"] 690 full_args += list(args) 691 subprocess.run( 692 full_args, 693 check=True, 694 ) 695 696 697def _do_ensure( 698 dep_specs: Sequence[str], 699 online: bool = False, 700 wheels_dir: Optional[Union[str, Path]] = None, 701) -> None: 702 """ 703 Use pip to ensure we have the package specified by @dep_specs. 704 705 If the package is already installed, do nothing. If online and 706 wheels_dir are both provided, prefer packages found in wheels_dir 707 first before connecting to PyPI. 708 709 :param dep_specs: 710 PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. 711 :param online: If True, fall back to PyPI. 712 :param wheels_dir: If specified, search this path for packages. 713 """ 714 with warnings.catch_warnings(): 715 warnings.filterwarnings( 716 "ignore", category=UserWarning, module="distlib" 717 ) 718 dist_path = distlib.database.DistributionPath(include_egg=True) 719 absent = [] 720 present = [] 721 for spec in dep_specs: 722 matcher = distlib.version.LegacyMatcher(spec) 723 dist = dist_path.get_distribution(matcher.name) 724 if dist is None or not matcher.match(dist.version): 725 absent.append(spec) 726 else: 727 logger.info("found %s", dist) 728 present.append(matcher.name) 729 730 if present: 731 generate_console_scripts(present) 732 733 if absent: 734 # Some packages are missing or aren't a suitable version, 735 # install a suitable (possibly vendored) package. 736 print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr) 737 pip_install(args=absent, online=online, wheels_dir=wheels_dir) 738 739 740def ensure( 741 dep_specs: Sequence[str], 742 online: bool = False, 743 wheels_dir: Optional[Union[str, Path]] = None, 744 prog: Optional[str] = None, 745) -> None: 746 """ 747 Use pip to ensure we have the package specified by @dep_specs. 748 749 If the package is already installed, do nothing. If online and 750 wheels_dir are both provided, prefer packages found in wheels_dir 751 first before connecting to PyPI. 752 753 :param dep_specs: 754 PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. 755 :param online: If True, fall back to PyPI. 756 :param wheels_dir: If specified, search this path for packages. 757 :param prog: 758 If specified, use this program name for error diagnostics that will 759 be presented to the user. e.g., 'sphinx-build' can be used as a 760 bellwether for the presence of 'sphinx'. 761 """ 762 print(f"mkvenv: checking for {', '.join(dep_specs)}", file=sys.stderr) 763 764 if not HAVE_DISTLIB: 765 raise Ouch("a usable distlib could not be found, please install it") 766 767 try: 768 _do_ensure(dep_specs, online, wheels_dir) 769 except subprocess.CalledProcessError as exc: 770 # Well, that's not good. 771 msg, bad = diagnose(dep_specs[0], online, wheels_dir, prog) 772 if bad: 773 raise Ouch(msg) from exc 774 raise SystemExit(f"\n{msg}\n\n") from exc 775 776 777def post_venv_setup() -> None: 778 """ 779 This is intended to be run *inside the venv* after it is created. 780 """ 781 logger.debug("post_venv_setup()") 782 # Test for a broken pip (Debian 10 or derivative?) and fix it if needed 783 if not checkpip(): 784 # Finally, generate a 'pip' script so the venv is usable in a normal 785 # way from the CLI. This only happens when we inherited pip from a 786 # parent/system-site and haven't run ensurepip in some way. 787 generate_console_scripts(["pip"]) 788 789 790def _add_create_subcommand(subparsers: Any) -> None: 791 subparser = subparsers.add_parser("create", help="create a venv") 792 subparser.add_argument( 793 "target", 794 type=str, 795 action="store", 796 help="Target directory to install virtual environment into.", 797 ) 798 799 800def _add_post_init_subcommand(subparsers: Any) -> None: 801 subparsers.add_parser("post_init", help="post-venv initialization") 802 803 804def _add_ensure_subcommand(subparsers: Any) -> None: 805 subparser = subparsers.add_parser( 806 "ensure", help="Ensure that the specified package is installed." 807 ) 808 subparser.add_argument( 809 "--online", 810 action="store_true", 811 help="Install packages from PyPI, if necessary.", 812 ) 813 subparser.add_argument( 814 "--dir", 815 type=str, 816 action="store", 817 help="Path to vendored packages where we may install from.", 818 ) 819 subparser.add_argument( 820 "--diagnose", 821 type=str, 822 action="store", 823 help=( 824 "Name of a shell utility to use for " 825 "diagnostics if this command fails." 826 ), 827 ) 828 subparser.add_argument( 829 "dep_specs", 830 type=str, 831 action="store", 832 help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'", 833 nargs="+", 834 ) 835 836 837def main() -> int: 838 """CLI interface to make_qemu_venv. See module docstring.""" 839 if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"): 840 # You're welcome. 841 logging.basicConfig(level=logging.DEBUG) 842 else: 843 if os.environ.get("V"): 844 logging.basicConfig(level=logging.INFO) 845 846 # These are incredibly noisy even for V=1 847 logging.getLogger("distlib.metadata").addFilter(lambda record: False) 848 logging.getLogger("distlib.database").addFilter(lambda record: False) 849 850 parser = argparse.ArgumentParser( 851 prog="mkvenv", 852 description="QEMU pyvenv bootstrapping utility", 853 ) 854 subparsers = parser.add_subparsers( 855 title="Commands", 856 dest="command", 857 required=True, 858 metavar="command", 859 help="Description", 860 ) 861 862 _add_create_subcommand(subparsers) 863 _add_post_init_subcommand(subparsers) 864 _add_ensure_subcommand(subparsers) 865 866 args = parser.parse_args() 867 try: 868 if args.command == "create": 869 make_venv( 870 args.target, 871 system_site_packages=True, 872 clear=True, 873 ) 874 if args.command == "post_init": 875 post_venv_setup() 876 if args.command == "ensure": 877 ensure( 878 dep_specs=args.dep_specs, 879 online=args.online, 880 wheels_dir=args.dir, 881 prog=args.diagnose, 882 ) 883 logger.debug("mkvenv.py %s: exiting", args.command) 884 except Ouch as exc: 885 print("\n*** Ouch! ***\n", file=sys.stderr) 886 print(str(exc), "\n\n", file=sys.stderr) 887 return 1 888 except SystemExit: 889 raise 890 except: # pylint: disable=bare-except 891 logger.exception("mkvenv did not complete successfully:") 892 return 2 893 return 0 894 895 896if __name__ == "__main__": 897 sys.exit(main()) 898