 a9dbde71da
			
		
	
	
		a9dbde71da
		
	
	
	
	
		
			
			Debian debundles ensurepip for python; NetBSD debundles pyexpat but ensurepip needs pyexpat. Try our best to offer a helpful error message instead of just failing catastrophically. Signed-off-by: John Snow <jsnow@redhat.com> Message-Id: <20230511035435.734312-5-jsnow@redhat.com> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
		
			
				
	
	
		
			285 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| mkvenv - QEMU pyvenv bootstrapping utility
 | |
| 
 | |
| usage: mkvenv [-h] command ...
 | |
| 
 | |
| QEMU pyvenv bootstrapping utility
 | |
| 
 | |
| options:
 | |
|   -h, --help  show this help message and exit
 | |
| 
 | |
| Commands:
 | |
|   command     Description
 | |
|     create    create a venv
 | |
| 
 | |
| --------------------------------------------------
 | |
| 
 | |
| usage: mkvenv create [-h] target
 | |
| 
 | |
| positional arguments:
 | |
|   target      Target directory to install virtual environment into.
 | |
| 
 | |
| options:
 | |
|   -h, --help  show this help message and exit
 | |
| 
 | |
| """
 | |
| 
 | |
| # Copyright (C) 2022-2023 Red Hat, Inc.
 | |
| #
 | |
| # Authors:
 | |
| #  John Snow <jsnow@redhat.com>
 | |
| #  Paolo Bonzini <pbonzini@redhat.com>
 | |
| #
 | |
| # This work is licensed under the terms of the GNU GPL, version 2 or
 | |
| # later. See the COPYING file in the top-level directory.
 | |
| 
 | |
| import argparse
 | |
| from importlib.util import find_spec
 | |
| import logging
 | |
| import os
 | |
| from pathlib import Path
 | |
| import subprocess
 | |
| import sys
 | |
| from types import SimpleNamespace
 | |
| from typing import Any, Optional, Union
 | |
| import venv
 | |
| 
 | |
| 
 | |
| # Do not add any mandatory dependencies from outside the stdlib:
 | |
| # This script *must* be usable standalone!
 | |
| 
 | |
| DirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
 | |
| logger = logging.getLogger("mkvenv")
 | |
| 
 | |
| 
 | |
| class Ouch(RuntimeError):
 | |
|     """An Exception class we can't confuse with a builtin."""
 | |
| 
 | |
| 
 | |
| class QemuEnvBuilder(venv.EnvBuilder):
 | |
|     """
 | |
|     An extension of venv.EnvBuilder for building QEMU's configure-time venv.
 | |
| 
 | |
|     As of this commit, it does not yet do anything particularly
 | |
|     different than the standard venv-creation utility. The next several
 | |
|     commits will gradually change that in small commits that highlight
 | |
|     each feature individually.
 | |
| 
 | |
|     Parameters for base class init:
 | |
|       - system_site_packages: bool = False
 | |
|       - clear: bool = False
 | |
|       - symlinks: bool = False
 | |
|       - upgrade: bool = False
 | |
|       - with_pip: bool = False
 | |
|       - prompt: Optional[str] = None
 | |
|       - upgrade_deps: bool = False             (Since 3.9)
 | |
|     """
 | |
| 
 | |
|     def __init__(self, *args: Any, **kwargs: Any) -> None:
 | |
|         logger.debug("QemuEnvBuilder.__init__(...)")
 | |
| 
 | |
|         if kwargs.get("with_pip", False):
 | |
|             check_ensurepip()
 | |
| 
 | |
|         super().__init__(*args, **kwargs)
 | |
| 
 | |
|         # Make the context available post-creation:
 | |
|         self._context: Optional[SimpleNamespace] = None
 | |
| 
 | |
|     def ensure_directories(self, env_dir: DirType) -> SimpleNamespace:
 | |
|         logger.debug("ensure_directories(env_dir=%s)", env_dir)
 | |
|         self._context = super().ensure_directories(env_dir)
 | |
|         return self._context
 | |
| 
 | |
|     def get_value(self, field: str) -> str:
 | |
|         """
 | |
|         Get a string value from the context namespace after a call to build.
 | |
| 
 | |
|         For valid field names, see:
 | |
|         https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories
 | |
|         """
 | |
|         ret = getattr(self._context, field)
 | |
|         assert isinstance(ret, str)
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| def check_ensurepip() -> None:
 | |
|     """
 | |
|     Check that we have ensurepip.
 | |
| 
 | |
|     Raise a fatal exception with a helpful hint if it isn't available.
 | |
|     """
 | |
|     if not find_spec("ensurepip"):
 | |
|         msg = (
 | |
|             "Python's ensurepip module is not found.\n"
 | |
|             "It's normally part of the Python standard library, "
 | |
|             "maybe your distribution packages it separately?\n"
 | |
|             "Either install ensurepip, or alleviate the need for it in the "
 | |
|             "first place by installing pip and setuptools for "
 | |
|             f"'{sys.executable}'.\n"
 | |
|             "(Hint: Debian puts ensurepip in its python3-venv package.)"
 | |
|         )
 | |
|         raise Ouch(msg)
 | |
| 
 | |
|     # ensurepip uses pyexpat, which can also go missing on us:
 | |
|     if not find_spec("pyexpat"):
 | |
|         msg = (
 | |
|             "Python's pyexpat module is not found.\n"
 | |
|             "It's normally part of the Python standard library, "
 | |
|             "maybe your distribution packages it separately?\n"
 | |
|             "Either install pyexpat, or alleviate the need for it in the "
 | |
|             "first place by installing pip and setuptools for "
 | |
|             f"'{sys.executable}'.\n\n"
 | |
|             "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)"
 | |
|         )
 | |
|         raise Ouch(msg)
 | |
| 
 | |
| 
 | |
| def make_venv(  # pylint: disable=too-many-arguments
 | |
|     env_dir: Union[str, Path],
 | |
|     system_site_packages: bool = False,
 | |
|     clear: bool = True,
 | |
|     symlinks: Optional[bool] = None,
 | |
|     with_pip: bool = True,
 | |
| ) -> None:
 | |
|     """
 | |
|     Create a venv using `QemuEnvBuilder`.
 | |
| 
 | |
|     This is analogous to the `venv.create` module-level convenience
 | |
|     function that is part of the Python stdblib, except it uses
 | |
|     `QemuEnvBuilder` instead.
 | |
| 
 | |
|     :param env_dir: The directory to create/install to.
 | |
|     :param system_site_packages:
 | |
|         Allow inheriting packages from the system installation.
 | |
|     :param clear: When True, fully remove any prior venv and files.
 | |
|     :param symlinks:
 | |
|         Whether to use symlinks to the target interpreter or not. If
 | |
|         left unspecified, it will use symlinks except on Windows to
 | |
|         match behavior with the "venv" CLI tool.
 | |
|     :param with_pip:
 | |
|         Whether to install "pip" binaries or not.
 | |
|     """
 | |
|     logger.debug(
 | |
|         "%s: make_venv(env_dir=%s, system_site_packages=%s, "
 | |
|         "clear=%s, symlinks=%s, with_pip=%s)",
 | |
|         __file__,
 | |
|         str(env_dir),
 | |
|         system_site_packages,
 | |
|         clear,
 | |
|         symlinks,
 | |
|         with_pip,
 | |
|     )
 | |
| 
 | |
|     if symlinks is None:
 | |
|         # Default behavior of standard venv CLI
 | |
|         symlinks = os.name != "nt"
 | |
| 
 | |
|     builder = QemuEnvBuilder(
 | |
|         system_site_packages=system_site_packages,
 | |
|         clear=clear,
 | |
|         symlinks=symlinks,
 | |
|         with_pip=with_pip,
 | |
|     )
 | |
| 
 | |
|     style = "non-isolated" if builder.system_site_packages else "isolated"
 | |
|     print(
 | |
|         f"mkvenv: Creating {style} virtual environment"
 | |
|         f" at '{str(env_dir)}'",
 | |
|         file=sys.stderr,
 | |
|     )
 | |
| 
 | |
|     try:
 | |
|         logger.debug("Invoking builder.create()")
 | |
|         try:
 | |
|             builder.create(str(env_dir))
 | |
|         except SystemExit as exc:
 | |
|             # Some versions of the venv module raise SystemExit; *nasty*!
 | |
|             # We want the exception that prompted it. It might be a subprocess
 | |
|             # error that has output we *really* want to see.
 | |
|             logger.debug("Intercepted SystemExit from EnvBuilder.create()")
 | |
|             raise exc.__cause__ or exc.__context__ or exc
 | |
|         logger.debug("builder.create() finished")
 | |
|     except subprocess.CalledProcessError as exc:
 | |
|         logger.error("mkvenv subprocess failed:")
 | |
|         logger.error("cmd: %s", exc.cmd)
 | |
|         logger.error("returncode: %d", exc.returncode)
 | |
| 
 | |
|         def _stringify(data: Union[str, bytes]) -> str:
 | |
|             if isinstance(data, bytes):
 | |
|                 return data.decode()
 | |
|             return data
 | |
| 
 | |
|         lines = []
 | |
|         if exc.stdout:
 | |
|             lines.append("========== stdout ==========")
 | |
|             lines.append(_stringify(exc.stdout))
 | |
|             lines.append("============================")
 | |
|         if exc.stderr:
 | |
|             lines.append("========== stderr ==========")
 | |
|             lines.append(_stringify(exc.stderr))
 | |
|             lines.append("============================")
 | |
|         if lines:
 | |
|             logger.error(os.linesep.join(lines))
 | |
| 
 | |
|         raise Ouch("VENV creation subprocess failed.") from exc
 | |
| 
 | |
|     # print the python executable to stdout for configure.
 | |
|     print(builder.get_value("env_exe"))
 | |
| 
 | |
| 
 | |
| def _add_create_subcommand(subparsers: Any) -> None:
 | |
|     subparser = subparsers.add_parser("create", help="create a venv")
 | |
|     subparser.add_argument(
 | |
|         "target",
 | |
|         type=str,
 | |
|         action="store",
 | |
|         help="Target directory to install virtual environment into.",
 | |
|     )
 | |
| 
 | |
| 
 | |
| def main() -> int:
 | |
|     """CLI interface to make_qemu_venv. See module docstring."""
 | |
|     if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
 | |
|         # You're welcome.
 | |
|         logging.basicConfig(level=logging.DEBUG)
 | |
|     elif os.environ.get("V"):
 | |
|         logging.basicConfig(level=logging.INFO)
 | |
| 
 | |
|     parser = argparse.ArgumentParser(
 | |
|         prog="mkvenv",
 | |
|         description="QEMU pyvenv bootstrapping utility",
 | |
|     )
 | |
|     subparsers = parser.add_subparsers(
 | |
|         title="Commands",
 | |
|         dest="command",
 | |
|         metavar="command",
 | |
|         help="Description",
 | |
|     )
 | |
| 
 | |
|     _add_create_subcommand(subparsers)
 | |
| 
 | |
|     args = parser.parse_args()
 | |
|     try:
 | |
|         if args.command == "create":
 | |
|             make_venv(
 | |
|                 args.target,
 | |
|                 system_site_packages=True,
 | |
|                 clear=True,
 | |
|             )
 | |
|         logger.debug("mkvenv.py %s: exiting", args.command)
 | |
|     except Ouch as exc:
 | |
|         print("\n*** Ouch! ***\n", file=sys.stderr)
 | |
|         print(str(exc), "\n\n", file=sys.stderr)
 | |
|         return 1
 | |
|     except SystemExit:
 | |
|         raise
 | |
|     except:  # pylint: disable=bare-except
 | |
|         logger.exception("mkvenv did not complete successfully:")
 | |
|         return 2
 | |
|     return 0
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     sys.exit(main())
 |