mkvenv.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. """
  2. mkvenv - QEMU pyvenv bootstrapping utility
  3. usage: mkvenv [-h] command ...
  4. QEMU pyvenv bootstrapping utility
  5. options:
  6. -h, --help show this help message and exit
  7. Commands:
  8. command Description
  9. create create a venv
  10. --------------------------------------------------
  11. usage: mkvenv create [-h] target
  12. positional arguments:
  13. target Target directory to install virtual environment into.
  14. options:
  15. -h, --help show this help message and exit
  16. """
  17. # Copyright (C) 2022-2023 Red Hat, Inc.
  18. #
  19. # Authors:
  20. # John Snow <jsnow@redhat.com>
  21. # Paolo Bonzini <pbonzini@redhat.com>
  22. #
  23. # This work is licensed under the terms of the GNU GPL, version 2 or
  24. # later. See the COPYING file in the top-level directory.
  25. import argparse
  26. from importlib.util import find_spec
  27. import logging
  28. import os
  29. from pathlib import Path
  30. import subprocess
  31. import sys
  32. from types import SimpleNamespace
  33. from typing import Any, Optional, Union
  34. import venv
  35. # Do not add any mandatory dependencies from outside the stdlib:
  36. # This script *must* be usable standalone!
  37. DirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
  38. logger = logging.getLogger("mkvenv")
  39. class Ouch(RuntimeError):
  40. """An Exception class we can't confuse with a builtin."""
  41. class QemuEnvBuilder(venv.EnvBuilder):
  42. """
  43. An extension of venv.EnvBuilder for building QEMU's configure-time venv.
  44. As of this commit, it does not yet do anything particularly
  45. different than the standard venv-creation utility. The next several
  46. commits will gradually change that in small commits that highlight
  47. each feature individually.
  48. Parameters for base class init:
  49. - system_site_packages: bool = False
  50. - clear: bool = False
  51. - symlinks: bool = False
  52. - upgrade: bool = False
  53. - with_pip: bool = False
  54. - prompt: Optional[str] = None
  55. - upgrade_deps: bool = False (Since 3.9)
  56. """
  57. def __init__(self, *args: Any, **kwargs: Any) -> None:
  58. logger.debug("QemuEnvBuilder.__init__(...)")
  59. if kwargs.get("with_pip", False):
  60. check_ensurepip()
  61. super().__init__(*args, **kwargs)
  62. # Make the context available post-creation:
  63. self._context: Optional[SimpleNamespace] = None
  64. def ensure_directories(self, env_dir: DirType) -> SimpleNamespace:
  65. logger.debug("ensure_directories(env_dir=%s)", env_dir)
  66. self._context = super().ensure_directories(env_dir)
  67. return self._context
  68. def get_value(self, field: str) -> str:
  69. """
  70. Get a string value from the context namespace after a call to build.
  71. For valid field names, see:
  72. https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories
  73. """
  74. ret = getattr(self._context, field)
  75. assert isinstance(ret, str)
  76. return ret
  77. def check_ensurepip() -> None:
  78. """
  79. Check that we have ensurepip.
  80. Raise a fatal exception with a helpful hint if it isn't available.
  81. """
  82. if not find_spec("ensurepip"):
  83. msg = (
  84. "Python's ensurepip module is not found.\n"
  85. "It's normally part of the Python standard library, "
  86. "maybe your distribution packages it separately?\n"
  87. "Either install ensurepip, or alleviate the need for it in the "
  88. "first place by installing pip and setuptools for "
  89. f"'{sys.executable}'.\n"
  90. "(Hint: Debian puts ensurepip in its python3-venv package.)"
  91. )
  92. raise Ouch(msg)
  93. # ensurepip uses pyexpat, which can also go missing on us:
  94. if not find_spec("pyexpat"):
  95. msg = (
  96. "Python's pyexpat module is not found.\n"
  97. "It's normally part of the Python standard library, "
  98. "maybe your distribution packages it separately?\n"
  99. "Either install pyexpat, or alleviate the need for it in the "
  100. "first place by installing pip and setuptools for "
  101. f"'{sys.executable}'.\n\n"
  102. "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)"
  103. )
  104. raise Ouch(msg)
  105. def make_venv( # pylint: disable=too-many-arguments
  106. env_dir: Union[str, Path],
  107. system_site_packages: bool = False,
  108. clear: bool = True,
  109. symlinks: Optional[bool] = None,
  110. with_pip: bool = True,
  111. ) -> None:
  112. """
  113. Create a venv using `QemuEnvBuilder`.
  114. This is analogous to the `venv.create` module-level convenience
  115. function that is part of the Python stdblib, except it uses
  116. `QemuEnvBuilder` instead.
  117. :param env_dir: The directory to create/install to.
  118. :param system_site_packages:
  119. Allow inheriting packages from the system installation.
  120. :param clear: When True, fully remove any prior venv and files.
  121. :param symlinks:
  122. Whether to use symlinks to the target interpreter or not. If
  123. left unspecified, it will use symlinks except on Windows to
  124. match behavior with the "venv" CLI tool.
  125. :param with_pip:
  126. Whether to install "pip" binaries or not.
  127. """
  128. logger.debug(
  129. "%s: make_venv(env_dir=%s, system_site_packages=%s, "
  130. "clear=%s, symlinks=%s, with_pip=%s)",
  131. __file__,
  132. str(env_dir),
  133. system_site_packages,
  134. clear,
  135. symlinks,
  136. with_pip,
  137. )
  138. if symlinks is None:
  139. # Default behavior of standard venv CLI
  140. symlinks = os.name != "nt"
  141. builder = QemuEnvBuilder(
  142. system_site_packages=system_site_packages,
  143. clear=clear,
  144. symlinks=symlinks,
  145. with_pip=with_pip,
  146. )
  147. style = "non-isolated" if builder.system_site_packages else "isolated"
  148. print(
  149. f"mkvenv: Creating {style} virtual environment"
  150. f" at '{str(env_dir)}'",
  151. file=sys.stderr,
  152. )
  153. try:
  154. logger.debug("Invoking builder.create()")
  155. try:
  156. builder.create(str(env_dir))
  157. except SystemExit as exc:
  158. # Some versions of the venv module raise SystemExit; *nasty*!
  159. # We want the exception that prompted it. It might be a subprocess
  160. # error that has output we *really* want to see.
  161. logger.debug("Intercepted SystemExit from EnvBuilder.create()")
  162. raise exc.__cause__ or exc.__context__ or exc
  163. logger.debug("builder.create() finished")
  164. except subprocess.CalledProcessError as exc:
  165. logger.error("mkvenv subprocess failed:")
  166. logger.error("cmd: %s", exc.cmd)
  167. logger.error("returncode: %d", exc.returncode)
  168. def _stringify(data: Union[str, bytes]) -> str:
  169. if isinstance(data, bytes):
  170. return data.decode()
  171. return data
  172. lines = []
  173. if exc.stdout:
  174. lines.append("========== stdout ==========")
  175. lines.append(_stringify(exc.stdout))
  176. lines.append("============================")
  177. if exc.stderr:
  178. lines.append("========== stderr ==========")
  179. lines.append(_stringify(exc.stderr))
  180. lines.append("============================")
  181. if lines:
  182. logger.error(os.linesep.join(lines))
  183. raise Ouch("VENV creation subprocess failed.") from exc
  184. # print the python executable to stdout for configure.
  185. print(builder.get_value("env_exe"))
  186. def _add_create_subcommand(subparsers: Any) -> None:
  187. subparser = subparsers.add_parser("create", help="create a venv")
  188. subparser.add_argument(
  189. "target",
  190. type=str,
  191. action="store",
  192. help="Target directory to install virtual environment into.",
  193. )
  194. def main() -> int:
  195. """CLI interface to make_qemu_venv. See module docstring."""
  196. if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"):
  197. # You're welcome.
  198. logging.basicConfig(level=logging.DEBUG)
  199. elif os.environ.get("V"):
  200. logging.basicConfig(level=logging.INFO)
  201. parser = argparse.ArgumentParser(
  202. prog="mkvenv",
  203. description="QEMU pyvenv bootstrapping utility",
  204. )
  205. subparsers = parser.add_subparsers(
  206. title="Commands",
  207. dest="command",
  208. metavar="command",
  209. help="Description",
  210. )
  211. _add_create_subcommand(subparsers)
  212. args = parser.parse_args()
  213. try:
  214. if args.command == "create":
  215. make_venv(
  216. args.target,
  217. system_site_packages=True,
  218. clear=True,
  219. )
  220. logger.debug("mkvenv.py %s: exiting", args.command)
  221. except Ouch as exc:
  222. print("\n*** Ouch! ***\n", file=sys.stderr)
  223. print(str(exc), "\n\n", file=sys.stderr)
  224. return 1
  225. except SystemExit:
  226. raise
  227. except: # pylint: disable=bare-except
  228. logger.exception("mkvenv did not complete successfully:")
  229. return 2
  230. return 0
  231. if __name__ == "__main__":
  232. sys.exit(main())