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 14dd84028fSJohn Snow 15dd84028fSJohn Snow-------------------------------------------------- 16dd84028fSJohn Snow 17dd84028fSJohn Snowusage: mkvenv create [-h] target 18dd84028fSJohn Snow 19dd84028fSJohn Snowpositional arguments: 20dd84028fSJohn Snow target Target directory to install virtual environment into. 21dd84028fSJohn Snow 22dd84028fSJohn Snowoptions: 23dd84028fSJohn Snow -h, --help show this help message and exit 24dd84028fSJohn Snow 25dd84028fSJohn Snow""" 26dd84028fSJohn Snow 27dd84028fSJohn Snow# Copyright (C) 2022-2023 Red Hat, Inc. 28dd84028fSJohn Snow# 29dd84028fSJohn Snow# Authors: 30dd84028fSJohn Snow# John Snow <jsnow@redhat.com> 31dd84028fSJohn Snow# Paolo Bonzini <pbonzini@redhat.com> 32dd84028fSJohn Snow# 33dd84028fSJohn Snow# This work is licensed under the terms of the GNU GPL, version 2 or 34dd84028fSJohn Snow# later. See the COPYING file in the top-level directory. 35dd84028fSJohn Snow 36dd84028fSJohn Snowimport argparse 37a9dbde71SJohn Snowfrom importlib.util import find_spec 38dd84028fSJohn Snowimport logging 39dd84028fSJohn Snowimport os 40dd84028fSJohn Snowfrom pathlib import Path 41*dee01b82SJohn Snowimport site 42dd84028fSJohn Snowimport subprocess 43dd84028fSJohn Snowimport sys 44*dee01b82SJohn Snowimport sysconfig 45dd84028fSJohn Snowfrom types import SimpleNamespace 46dd84028fSJohn Snowfrom typing import Any, Optional, Union 47dd84028fSJohn Snowimport venv 48dd84028fSJohn Snow 49dd84028fSJohn Snow 50dd84028fSJohn Snow# Do not add any mandatory dependencies from outside the stdlib: 51dd84028fSJohn Snow# This script *must* be usable standalone! 52dd84028fSJohn Snow 53dd84028fSJohn SnowDirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] 54dd84028fSJohn Snowlogger = logging.getLogger("mkvenv") 55dd84028fSJohn Snow 56dd84028fSJohn Snow 57*dee01b82SJohn Snowdef inside_a_venv() -> bool: 58*dee01b82SJohn Snow """Returns True if it is executed inside of a virtual environment.""" 59*dee01b82SJohn Snow return sys.prefix != sys.base_prefix 60*dee01b82SJohn Snow 61*dee01b82SJohn Snow 62dd84028fSJohn Snowclass Ouch(RuntimeError): 63dd84028fSJohn Snow """An Exception class we can't confuse with a builtin.""" 64dd84028fSJohn Snow 65dd84028fSJohn Snow 66dd84028fSJohn Snowclass QemuEnvBuilder(venv.EnvBuilder): 67dd84028fSJohn Snow """ 68dd84028fSJohn Snow An extension of venv.EnvBuilder for building QEMU's configure-time venv. 69dd84028fSJohn Snow 70*dee01b82SJohn Snow The primary difference is that it emulates a "nested" virtual 71*dee01b82SJohn Snow environment when invoked from inside of an existing virtual 72*dee01b82SJohn Snow environment by including packages from the parent. 73dd84028fSJohn Snow 74dd84028fSJohn Snow Parameters for base class init: 75dd84028fSJohn Snow - system_site_packages: bool = False 76dd84028fSJohn Snow - clear: bool = False 77dd84028fSJohn Snow - symlinks: bool = False 78dd84028fSJohn Snow - upgrade: bool = False 79dd84028fSJohn Snow - with_pip: bool = False 80dd84028fSJohn Snow - prompt: Optional[str] = None 81dd84028fSJohn Snow - upgrade_deps: bool = False (Since 3.9) 82dd84028fSJohn Snow """ 83dd84028fSJohn Snow 84dd84028fSJohn Snow def __init__(self, *args: Any, **kwargs: Any) -> None: 85dd84028fSJohn Snow logger.debug("QemuEnvBuilder.__init__(...)") 86a9dbde71SJohn Snow 87*dee01b82SJohn Snow # For nested venv emulation: 88*dee01b82SJohn Snow self.use_parent_packages = False 89*dee01b82SJohn Snow if inside_a_venv(): 90*dee01b82SJohn Snow # Include parent packages only if we're in a venv and 91*dee01b82SJohn Snow # system_site_packages was True. 92*dee01b82SJohn Snow self.use_parent_packages = kwargs.pop( 93*dee01b82SJohn Snow "system_site_packages", False 94*dee01b82SJohn Snow ) 95*dee01b82SJohn Snow # Include system_site_packages only when the parent, 96*dee01b82SJohn Snow # The venv we are currently in, also does so. 97*dee01b82SJohn Snow kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES 98*dee01b82SJohn Snow 99a9dbde71SJohn Snow if kwargs.get("with_pip", False): 100a9dbde71SJohn Snow check_ensurepip() 101a9dbde71SJohn Snow 102dd84028fSJohn Snow super().__init__(*args, **kwargs) 103dd84028fSJohn Snow 104dd84028fSJohn Snow # Make the context available post-creation: 105dd84028fSJohn Snow self._context: Optional[SimpleNamespace] = None 106dd84028fSJohn Snow 107*dee01b82SJohn Snow def get_parent_libpath(self) -> Optional[str]: 108*dee01b82SJohn Snow """Return the libpath of the parent venv, if applicable.""" 109*dee01b82SJohn Snow if self.use_parent_packages: 110*dee01b82SJohn Snow return sysconfig.get_path("purelib") 111*dee01b82SJohn Snow return None 112*dee01b82SJohn Snow 113*dee01b82SJohn Snow @staticmethod 114*dee01b82SJohn Snow def compute_venv_libpath(context: SimpleNamespace) -> str: 115*dee01b82SJohn Snow """ 116*dee01b82SJohn Snow Compatibility wrapper for context.lib_path for Python < 3.12 117*dee01b82SJohn Snow """ 118*dee01b82SJohn Snow # Python 3.12+, not strictly necessary because it's documented 119*dee01b82SJohn Snow # to be the same as 3.10 code below: 120*dee01b82SJohn Snow if sys.version_info >= (3, 12): 121*dee01b82SJohn Snow return context.lib_path 122*dee01b82SJohn Snow 123*dee01b82SJohn Snow # Python 3.10+ 124*dee01b82SJohn Snow if "venv" in sysconfig.get_scheme_names(): 125*dee01b82SJohn Snow lib_path = sysconfig.get_path( 126*dee01b82SJohn Snow "purelib", scheme="venv", vars={"base": context.env_dir} 127*dee01b82SJohn Snow ) 128*dee01b82SJohn Snow assert lib_path is not None 129*dee01b82SJohn Snow return lib_path 130*dee01b82SJohn Snow 131*dee01b82SJohn Snow # For Python <= 3.9 we need to hardcode this. Fortunately the 132*dee01b82SJohn Snow # code below was the same in Python 3.6-3.10, so there is only 133*dee01b82SJohn Snow # one case. 134*dee01b82SJohn Snow if sys.platform == "win32": 135*dee01b82SJohn Snow return os.path.join(context.env_dir, "Lib", "site-packages") 136*dee01b82SJohn Snow return os.path.join( 137*dee01b82SJohn Snow context.env_dir, 138*dee01b82SJohn Snow "lib", 139*dee01b82SJohn Snow "python%d.%d" % sys.version_info[:2], 140*dee01b82SJohn Snow "site-packages", 141*dee01b82SJohn Snow ) 142*dee01b82SJohn Snow 143dd84028fSJohn Snow def ensure_directories(self, env_dir: DirType) -> SimpleNamespace: 144dd84028fSJohn Snow logger.debug("ensure_directories(env_dir=%s)", env_dir) 145dd84028fSJohn Snow self._context = super().ensure_directories(env_dir) 146dd84028fSJohn Snow return self._context 147dd84028fSJohn Snow 148*dee01b82SJohn Snow def create(self, env_dir: DirType) -> None: 149*dee01b82SJohn Snow logger.debug("create(env_dir=%s)", env_dir) 150*dee01b82SJohn Snow super().create(env_dir) 151*dee01b82SJohn Snow assert self._context is not None 152*dee01b82SJohn Snow self.post_post_setup(self._context) 153*dee01b82SJohn Snow 154*dee01b82SJohn Snow def post_post_setup(self, context: SimpleNamespace) -> None: 155*dee01b82SJohn Snow """ 156*dee01b82SJohn Snow The final, final hook. Enter the venv and run commands inside of it. 157*dee01b82SJohn Snow """ 158*dee01b82SJohn Snow if self.use_parent_packages: 159*dee01b82SJohn Snow # We're inside of a venv and we want to include the parent 160*dee01b82SJohn Snow # venv's packages. 161*dee01b82SJohn Snow parent_libpath = self.get_parent_libpath() 162*dee01b82SJohn Snow assert parent_libpath is not None 163*dee01b82SJohn Snow logger.debug("parent_libpath: %s", parent_libpath) 164*dee01b82SJohn Snow 165*dee01b82SJohn Snow our_libpath = self.compute_venv_libpath(context) 166*dee01b82SJohn Snow logger.debug("our_libpath: %s", our_libpath) 167*dee01b82SJohn Snow 168*dee01b82SJohn Snow pth_file = os.path.join(our_libpath, "nested.pth") 169*dee01b82SJohn Snow with open(pth_file, "w", encoding="UTF-8") as file: 170*dee01b82SJohn Snow file.write(parent_libpath + os.linesep) 171*dee01b82SJohn Snow 172dd84028fSJohn Snow def get_value(self, field: str) -> str: 173dd84028fSJohn Snow """ 174dd84028fSJohn Snow Get a string value from the context namespace after a call to build. 175dd84028fSJohn Snow 176dd84028fSJohn Snow For valid field names, see: 177dd84028fSJohn Snow https://docs.python.org/3/library/venv.html#venv.EnvBuilder.ensure_directories 178dd84028fSJohn Snow """ 179dd84028fSJohn Snow ret = getattr(self._context, field) 180dd84028fSJohn Snow assert isinstance(ret, str) 181dd84028fSJohn Snow return ret 182dd84028fSJohn Snow 183dd84028fSJohn Snow 184a9dbde71SJohn Snowdef check_ensurepip() -> None: 185a9dbde71SJohn Snow """ 186a9dbde71SJohn Snow Check that we have ensurepip. 187a9dbde71SJohn Snow 188a9dbde71SJohn Snow Raise a fatal exception with a helpful hint if it isn't available. 189a9dbde71SJohn Snow """ 190a9dbde71SJohn Snow if not find_spec("ensurepip"): 191a9dbde71SJohn Snow msg = ( 192a9dbde71SJohn Snow "Python's ensurepip module is not found.\n" 193a9dbde71SJohn Snow "It's normally part of the Python standard library, " 194a9dbde71SJohn Snow "maybe your distribution packages it separately?\n" 195a9dbde71SJohn Snow "Either install ensurepip, or alleviate the need for it in the " 196a9dbde71SJohn Snow "first place by installing pip and setuptools for " 197a9dbde71SJohn Snow f"'{sys.executable}'.\n" 198a9dbde71SJohn Snow "(Hint: Debian puts ensurepip in its python3-venv package.)" 199a9dbde71SJohn Snow ) 200a9dbde71SJohn Snow raise Ouch(msg) 201a9dbde71SJohn Snow 202a9dbde71SJohn Snow # ensurepip uses pyexpat, which can also go missing on us: 203a9dbde71SJohn Snow if not find_spec("pyexpat"): 204a9dbde71SJohn Snow msg = ( 205a9dbde71SJohn Snow "Python's pyexpat module is not found.\n" 206a9dbde71SJohn Snow "It's normally part of the Python standard library, " 207a9dbde71SJohn Snow "maybe your distribution packages it separately?\n" 208a9dbde71SJohn Snow "Either install pyexpat, or alleviate the need for it in the " 209a9dbde71SJohn Snow "first place by installing pip and setuptools for " 210a9dbde71SJohn Snow f"'{sys.executable}'.\n\n" 211a9dbde71SJohn Snow "(Hint: NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)" 212a9dbde71SJohn Snow ) 213a9dbde71SJohn Snow raise Ouch(msg) 214a9dbde71SJohn Snow 215a9dbde71SJohn Snow 216dd84028fSJohn Snowdef make_venv( # pylint: disable=too-many-arguments 217dd84028fSJohn Snow env_dir: Union[str, Path], 218dd84028fSJohn Snow system_site_packages: bool = False, 219dd84028fSJohn Snow clear: bool = True, 220dd84028fSJohn Snow symlinks: Optional[bool] = None, 221dd84028fSJohn Snow with_pip: bool = True, 222dd84028fSJohn Snow) -> None: 223dd84028fSJohn Snow """ 224dd84028fSJohn Snow Create a venv using `QemuEnvBuilder`. 225dd84028fSJohn Snow 226dd84028fSJohn Snow This is analogous to the `venv.create` module-level convenience 227dd84028fSJohn Snow function that is part of the Python stdblib, except it uses 228dd84028fSJohn Snow `QemuEnvBuilder` instead. 229dd84028fSJohn Snow 230dd84028fSJohn Snow :param env_dir: The directory to create/install to. 231dd84028fSJohn Snow :param system_site_packages: 232dd84028fSJohn Snow Allow inheriting packages from the system installation. 233dd84028fSJohn Snow :param clear: When True, fully remove any prior venv and files. 234dd84028fSJohn Snow :param symlinks: 235dd84028fSJohn Snow Whether to use symlinks to the target interpreter or not. If 236dd84028fSJohn Snow left unspecified, it will use symlinks except on Windows to 237dd84028fSJohn Snow match behavior with the "venv" CLI tool. 238dd84028fSJohn Snow :param with_pip: 239dd84028fSJohn Snow Whether to install "pip" binaries or not. 240dd84028fSJohn Snow """ 241dd84028fSJohn Snow logger.debug( 242dd84028fSJohn Snow "%s: make_venv(env_dir=%s, system_site_packages=%s, " 243dd84028fSJohn Snow "clear=%s, symlinks=%s, with_pip=%s)", 244dd84028fSJohn Snow __file__, 245dd84028fSJohn Snow str(env_dir), 246dd84028fSJohn Snow system_site_packages, 247dd84028fSJohn Snow clear, 248dd84028fSJohn Snow symlinks, 249dd84028fSJohn Snow with_pip, 250dd84028fSJohn Snow ) 251dd84028fSJohn Snow 252dd84028fSJohn Snow if symlinks is None: 253dd84028fSJohn Snow # Default behavior of standard venv CLI 254dd84028fSJohn Snow symlinks = os.name != "nt" 255dd84028fSJohn Snow 256dd84028fSJohn Snow builder = QemuEnvBuilder( 257dd84028fSJohn Snow system_site_packages=system_site_packages, 258dd84028fSJohn Snow clear=clear, 259dd84028fSJohn Snow symlinks=symlinks, 260dd84028fSJohn Snow with_pip=with_pip, 261dd84028fSJohn Snow ) 262dd84028fSJohn Snow 263dd84028fSJohn Snow style = "non-isolated" if builder.system_site_packages else "isolated" 264*dee01b82SJohn Snow nested = "" 265*dee01b82SJohn Snow if builder.use_parent_packages: 266*dee01b82SJohn Snow nested = f"(with packages from '{builder.get_parent_libpath()}') " 267dd84028fSJohn Snow print( 268dd84028fSJohn Snow f"mkvenv: Creating {style} virtual environment" 269*dee01b82SJohn Snow f" {nested}at '{str(env_dir)}'", 270dd84028fSJohn Snow file=sys.stderr, 271dd84028fSJohn Snow ) 272dd84028fSJohn Snow 273dd84028fSJohn Snow try: 274dd84028fSJohn Snow logger.debug("Invoking builder.create()") 275dd84028fSJohn Snow try: 276dd84028fSJohn Snow builder.create(str(env_dir)) 277dd84028fSJohn Snow except SystemExit as exc: 278dd84028fSJohn Snow # Some versions of the venv module raise SystemExit; *nasty*! 279dd84028fSJohn Snow # We want the exception that prompted it. It might be a subprocess 280dd84028fSJohn Snow # error that has output we *really* want to see. 281dd84028fSJohn Snow logger.debug("Intercepted SystemExit from EnvBuilder.create()") 282dd84028fSJohn Snow raise exc.__cause__ or exc.__context__ or exc 283dd84028fSJohn Snow logger.debug("builder.create() finished") 284dd84028fSJohn Snow except subprocess.CalledProcessError as exc: 285dd84028fSJohn Snow logger.error("mkvenv subprocess failed:") 286dd84028fSJohn Snow logger.error("cmd: %s", exc.cmd) 287dd84028fSJohn Snow logger.error("returncode: %d", exc.returncode) 288dd84028fSJohn Snow 289dd84028fSJohn Snow def _stringify(data: Union[str, bytes]) -> str: 290dd84028fSJohn Snow if isinstance(data, bytes): 291dd84028fSJohn Snow return data.decode() 292dd84028fSJohn Snow return data 293dd84028fSJohn Snow 294dd84028fSJohn Snow lines = [] 295dd84028fSJohn Snow if exc.stdout: 296dd84028fSJohn Snow lines.append("========== stdout ==========") 297dd84028fSJohn Snow lines.append(_stringify(exc.stdout)) 298dd84028fSJohn Snow lines.append("============================") 299dd84028fSJohn Snow if exc.stderr: 300dd84028fSJohn Snow lines.append("========== stderr ==========") 301dd84028fSJohn Snow lines.append(_stringify(exc.stderr)) 302dd84028fSJohn Snow lines.append("============================") 303dd84028fSJohn Snow if lines: 304dd84028fSJohn Snow logger.error(os.linesep.join(lines)) 305dd84028fSJohn Snow 306dd84028fSJohn Snow raise Ouch("VENV creation subprocess failed.") from exc 307dd84028fSJohn Snow 308dd84028fSJohn Snow # print the python executable to stdout for configure. 309dd84028fSJohn Snow print(builder.get_value("env_exe")) 310dd84028fSJohn Snow 311dd84028fSJohn Snow 312dd84028fSJohn Snowdef _add_create_subcommand(subparsers: Any) -> None: 313dd84028fSJohn Snow subparser = subparsers.add_parser("create", help="create a venv") 314dd84028fSJohn Snow subparser.add_argument( 315dd84028fSJohn Snow "target", 316dd84028fSJohn Snow type=str, 317dd84028fSJohn Snow action="store", 318dd84028fSJohn Snow help="Target directory to install virtual environment into.", 319dd84028fSJohn Snow ) 320dd84028fSJohn Snow 321dd84028fSJohn Snow 322dd84028fSJohn Snowdef main() -> int: 323dd84028fSJohn Snow """CLI interface to make_qemu_venv. See module docstring.""" 324dd84028fSJohn Snow if os.environ.get("DEBUG") or os.environ.get("GITLAB_CI"): 325dd84028fSJohn Snow # You're welcome. 326dd84028fSJohn Snow logging.basicConfig(level=logging.DEBUG) 327dd84028fSJohn Snow elif os.environ.get("V"): 328dd84028fSJohn Snow logging.basicConfig(level=logging.INFO) 329dd84028fSJohn Snow 330dd84028fSJohn Snow parser = argparse.ArgumentParser( 331dd84028fSJohn Snow prog="mkvenv", 332dd84028fSJohn Snow description="QEMU pyvenv bootstrapping utility", 333dd84028fSJohn Snow ) 334dd84028fSJohn Snow subparsers = parser.add_subparsers( 335dd84028fSJohn Snow title="Commands", 336dd84028fSJohn Snow dest="command", 337dd84028fSJohn Snow metavar="command", 338dd84028fSJohn Snow help="Description", 339dd84028fSJohn Snow ) 340dd84028fSJohn Snow 341dd84028fSJohn Snow _add_create_subcommand(subparsers) 342dd84028fSJohn Snow 343dd84028fSJohn Snow args = parser.parse_args() 344dd84028fSJohn Snow try: 345dd84028fSJohn Snow if args.command == "create": 346dd84028fSJohn Snow make_venv( 347dd84028fSJohn Snow args.target, 348dd84028fSJohn Snow system_site_packages=True, 349dd84028fSJohn Snow clear=True, 350dd84028fSJohn Snow ) 351dd84028fSJohn Snow logger.debug("mkvenv.py %s: exiting", args.command) 352dd84028fSJohn Snow except Ouch as exc: 353dd84028fSJohn Snow print("\n*** Ouch! ***\n", file=sys.stderr) 354dd84028fSJohn Snow print(str(exc), "\n\n", file=sys.stderr) 355dd84028fSJohn Snow return 1 356dd84028fSJohn Snow except SystemExit: 357dd84028fSJohn Snow raise 358dd84028fSJohn Snow except: # pylint: disable=bare-except 359dd84028fSJohn Snow logger.exception("mkvenv did not complete successfully:") 360dd84028fSJohn Snow return 2 361dd84028fSJohn Snow return 0 362dd84028fSJohn Snow 363dd84028fSJohn Snow 364dd84028fSJohn Snowif __name__ == "__main__": 365dd84028fSJohn Snow sys.exit(main()) 366