From dd84028ff911e75028dea0ac1e578c2a7bcc3ffd Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 10 May 2023 23:54:11 -0400 Subject: python: add mkvenv.py This script will be responsible for building a lightweight Python virtual environment at configure time. It works with Python 3.6 or newer. It has been designed to: - work *offline*, no PyPI required. - work *quickly*, The fast path is only ~65ms on my machine. - work *robustly*, with multiple fallbacks to keep things working. - work *cooperatively*, using system packages where possible. (You can use your distro's meson, no problem.) Due to its unique position in the build chain, it exists outside of the installable python packages in-tree and *must* be runnable without any third party dependencies. Under normal circumstances, the only dependency required to execute this script is Python 3.6+ itself. The script is *faster* by several seconds when setuptools and pip are installed in the host environment, which is probably the case for a typical multi-purpose developer workstation. In the event that pip/setuptools are missing or not usable, additional dependencies may be required on some distributions which remove certain Python stdlib modules to package them separately: - Debian may require python3-venv to provide "ensurepip" - NetBSD may require py310-expat to provide "pyexpat" * (* Or whichever version is current for NetBSD.) Signed-off-by: Paolo Bonzini Signed-off-by: John Snow Message-Id: <20230511035435.734312-4-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 247 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 python/scripts/mkvenv.py (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py new file mode 100644 index 0000000..a4534e4 --- /dev/null +++ b/python/scripts/mkvenv.py @@ -0,0 +1,247 @@ +""" +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 +# Paolo Bonzini +# +# 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 +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__(...)") + 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 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()) -- cgit v1.1 From a9dbde71da553fe0b132ffac6d1a0de16892a90d Mon Sep 17 00:00:00 2001 From: John Snow Date: Tue, 16 May 2023 09:25:38 +0200 Subject: mkvenv: add better error message for broken or missing ensurepip 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 Message-Id: <20230511035435.734312-5-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index a4534e4..3c48f8b 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -34,6 +34,7 @@ options: # 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 @@ -76,6 +77,10 @@ class QemuEnvBuilder(venv.EnvBuilder): 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: @@ -98,6 +103,38 @@ class QemuEnvBuilder(venv.EnvBuilder): 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, -- cgit v1.1 From dee01b827ffc26577217697074052b8b7f4770dc Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 10 May 2023 23:54:13 -0400 Subject: mkvenv: add nested venv workaround Python virtual environments do not typically nest; they may inherit from the top-level system packages or not at all. For our purposes, it would be convenient to emulate "nested" virtual environments to allow callers of the configure script to install specific versions of python utilities in order to test build system features, utility version compatibility, etc. While it is possible to install packages into the system environment (say, by using the --user flag), it's nicer to install test packages into a totally isolated environment instead. As detailed in https://www.qemu.org/2023/03/24/python/, Emulate a nested venv environment by using .pth files installed into the site-packages folder that points to the parent environment when appropriate. Signed-off-by: Paolo Bonzini Signed-off-by: John Snow Message-Id: <20230511035435.734312-6-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 91 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 5 deletions(-) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index 3c48f8b..1c1dd89 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -38,8 +38,10 @@ from importlib.util import find_spec import logging import os from pathlib import Path +import site import subprocess import sys +import sysconfig from types import SimpleNamespace from typing import Any, Optional, Union import venv @@ -52,6 +54,11 @@ DirType = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] logger = logging.getLogger("mkvenv") +def inside_a_venv() -> bool: + """Returns True if it is executed inside of a virtual environment.""" + return sys.prefix != sys.base_prefix + + class Ouch(RuntimeError): """An Exception class we can't confuse with a builtin.""" @@ -60,10 +67,9 @@ 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. + The primary difference is that it emulates a "nested" virtual + environment when invoked from inside of an existing virtual + environment by including packages from the parent. Parameters for base class init: - system_site_packages: bool = False @@ -78,6 +84,18 @@ class QemuEnvBuilder(venv.EnvBuilder): def __init__(self, *args: Any, **kwargs: Any) -> None: logger.debug("QemuEnvBuilder.__init__(...)") + # For nested venv emulation: + self.use_parent_packages = False + if inside_a_venv(): + # Include parent packages only if we're in a venv and + # system_site_packages was True. + self.use_parent_packages = kwargs.pop( + "system_site_packages", False + ) + # Include system_site_packages only when the parent, + # The venv we are currently in, also does so. + kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES + if kwargs.get("with_pip", False): check_ensurepip() @@ -86,11 +104,71 @@ class QemuEnvBuilder(venv.EnvBuilder): # Make the context available post-creation: self._context: Optional[SimpleNamespace] = None + def get_parent_libpath(self) -> Optional[str]: + """Return the libpath of the parent venv, if applicable.""" + if self.use_parent_packages: + return sysconfig.get_path("purelib") + return None + + @staticmethod + def compute_venv_libpath(context: SimpleNamespace) -> str: + """ + Compatibility wrapper for context.lib_path for Python < 3.12 + """ + # Python 3.12+, not strictly necessary because it's documented + # to be the same as 3.10 code below: + if sys.version_info >= (3, 12): + return context.lib_path + + # Python 3.10+ + if "venv" in sysconfig.get_scheme_names(): + lib_path = sysconfig.get_path( + "purelib", scheme="venv", vars={"base": context.env_dir} + ) + assert lib_path is not None + return lib_path + + # For Python <= 3.9 we need to hardcode this. Fortunately the + # code below was the same in Python 3.6-3.10, so there is only + # one case. + if sys.platform == "win32": + return os.path.join(context.env_dir, "Lib", "site-packages") + return os.path.join( + context.env_dir, + "lib", + "python%d.%d" % sys.version_info[:2], + "site-packages", + ) + 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 create(self, env_dir: DirType) -> None: + logger.debug("create(env_dir=%s)", env_dir) + super().create(env_dir) + assert self._context is not None + self.post_post_setup(self._context) + + def post_post_setup(self, context: SimpleNamespace) -> None: + """ + The final, final hook. Enter the venv and run commands inside of it. + """ + if self.use_parent_packages: + # We're inside of a venv and we want to include the parent + # venv's packages. + parent_libpath = self.get_parent_libpath() + assert parent_libpath is not None + logger.debug("parent_libpath: %s", parent_libpath) + + our_libpath = self.compute_venv_libpath(context) + logger.debug("our_libpath: %s", our_libpath) + + pth_file = os.path.join(our_libpath, "nested.pth") + with open(pth_file, "w", encoding="UTF-8") as file: + file.write(parent_libpath + os.linesep) + def get_value(self, field: str) -> str: """ Get a string value from the context namespace after a call to build. @@ -183,9 +261,12 @@ def make_venv( # pylint: disable=too-many-arguments ) style = "non-isolated" if builder.system_site_packages else "isolated" + nested = "" + if builder.use_parent_packages: + nested = f"(with packages from '{builder.get_parent_libpath()}') " print( f"mkvenv: Creating {style} virtual environment" - f" at '{str(env_dir)}'", + f" {nested}at '{str(env_dir)}'", file=sys.stderr, ) -- cgit v1.1 From c5538eed12e2e427e3715b39cca46c00afcde78d Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 10 May 2023 23:54:14 -0400 Subject: mkvenv: add ensure subcommand This command is to be used to add various packages (or ensure they're already present) into the configure-provided venv in a modular fashion. Examples: mkvenv ensure --online --dir "${source_dir}/python/wheels/" "meson>=0.61.5" mkvenv ensure --online "sphinx>=1.6.0" mkvenv ensure "qemu.qmp==0.0.2" It's designed to look for packages in three places, in order: (1) In system packages, if the version installed is already good enough. This way your distribution-provided meson, sphinx, etc are always used as first preference. (2) In a vendored packages directory. Here I am suggesting qemu.git/python/wheels/ as that directory. This is intended to serve as a replacement for vendoring the meson source for QEMU tarballs. It is also highly likely to be extremely useful for packaging the "qemu.qmp" package in source distributions for platforms that do not yet package qemu.qmp separately. (3) Online, via PyPI, ***only when "--online" is passed***. This is only ever used as a fallback if the first two sources do not have an appropriate package that meets the requirement. The ability to build QEMU and run tests *completely offline* is not impinged. Signed-off-by: Paolo Bonzini Signed-off-by: John Snow Message-Id: <20230511035435.734312-7-jsnow@redhat.com> [Use distlib to lookup distributions. - Paolo] Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 135 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 3 deletions(-) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index 1c1dd89..f6c4f37 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -11,6 +11,7 @@ options: Commands: command Description create create a venv + ensure Ensure that the specified package is installed. -------------------------------------------------- @@ -22,6 +23,18 @@ positional arguments: options: -h, --help show this help message and exit +-------------------------------------------------- + +usage: mkvenv ensure [-h] [--online] [--dir DIR] dep_spec... + +positional arguments: + dep_spec PEP 508 Dependency specification, e.g. 'meson>=0.61.5' + +options: + -h, --help show this help message and exit + --online Install packages from PyPI, if necessary. + --dir DIR Path to vendored packages where we may install from. + """ # Copyright (C) 2022-2023 Red Hat, Inc. @@ -43,8 +56,17 @@ import subprocess import sys import sysconfig from types import SimpleNamespace -from typing import Any, Optional, Union +from typing import ( + Any, + Optional, + Sequence, + Union, +) import venv +import warnings + +import distlib.database +import distlib.version # Do not add any mandatory dependencies from outside the stdlib: @@ -309,6 +331,77 @@ def make_venv( # pylint: disable=too-many-arguments print(builder.get_value("env_exe")) +def pip_install( + args: Sequence[str], + online: bool = False, + wheels_dir: Optional[Union[str, Path]] = None, +) -> None: + """ + Use pip to install a package or package(s) as specified in @args. + """ + loud = bool( + os.environ.get("DEBUG") + or os.environ.get("GITLAB_CI") + or os.environ.get("V") + ) + + full_args = [ + sys.executable, + "-m", + "pip", + "install", + "--disable-pip-version-check", + "-v" if loud else "-q", + ] + if not online: + full_args += ["--no-index"] + if wheels_dir: + full_args += ["--find-links", f"file://{str(wheels_dir)}"] + full_args += list(args) + subprocess.run( + full_args, + check=True, + ) + + +def ensure( + dep_specs: Sequence[str], + online: bool = False, + wheels_dir: Optional[Union[str, Path]] = None, +) -> None: + """ + Use pip to ensure we have the package specified by @dep_specs. + + If the package is already installed, do nothing. If online and + wheels_dir are both provided, prefer packages found in wheels_dir + first before connecting to PyPI. + + :param dep_specs: + PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. + :param online: If True, fall back to PyPI. + :param wheels_dir: If specified, search this path for packages. + """ + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=UserWarning, module="distlib" + ) + dist_path = distlib.database.DistributionPath(include_egg=True) + absent = [] + for spec in dep_specs: + matcher = distlib.version.LegacyMatcher(spec) + dist = dist_path.get_distribution(matcher.name) + if dist is None or not matcher.match(dist.version): + absent.append(spec) + else: + logger.info("found %s", dist) + + if absent: + # Some packages are missing or aren't a suitable version, + # install a suitable (possibly vendored) package. + print(f"mkvenv: installing {', '.join(absent)}", file=sys.stderr) + pip_install(args=absent, online=online, wheels_dir=wheels_dir) + + def _add_create_subcommand(subparsers: Any) -> None: subparser = subparsers.add_parser("create", help="create a venv") subparser.add_argument( @@ -319,13 +412,42 @@ def _add_create_subcommand(subparsers: Any) -> None: ) +def _add_ensure_subcommand(subparsers: Any) -> None: + subparser = subparsers.add_parser( + "ensure", help="Ensure that the specified package is installed." + ) + subparser.add_argument( + "--online", + action="store_true", + help="Install packages from PyPI, if necessary.", + ) + subparser.add_argument( + "--dir", + type=str, + action="store", + help="Path to vendored packages where we may install from.", + ) + subparser.add_argument( + "dep_specs", + type=str, + action="store", + help="PEP 508 Dependency specification, e.g. 'meson>=0.61.5'", + nargs="+", + ) + + 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) + else: + if os.environ.get("V"): + logging.basicConfig(level=logging.INFO) + + # These are incredibly noisy even for V=1 + logging.getLogger("distlib.metadata").addFilter(lambda record: False) + logging.getLogger("distlib.database").addFilter(lambda record: False) parser = argparse.ArgumentParser( prog="mkvenv", @@ -339,6 +461,7 @@ def main() -> int: ) _add_create_subcommand(subparsers) + _add_ensure_subcommand(subparsers) args = parser.parse_args() try: @@ -348,6 +471,12 @@ def main() -> int: system_site_packages=True, clear=True, ) + if args.command == "ensure": + ensure( + dep_specs=args.dep_specs, + online=args.online, + wheels_dir=args.dir, + ) logger.debug("mkvenv.py %s: exiting", args.command) except Ouch as exc: print("\n*** Ouch! ***\n", file=sys.stderr) -- cgit v1.1 From 4695a22e9adb0c9a96465109a17da10c17cf2a67 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 10 May 2023 23:54:15 -0400 Subject: mkvenv: add --diagnose option to explain "ensure" failures This is a routine that is designed to print some usable info for human beings back out to the terminal if/when "mkvenv ensure" fails to locate or install a package during configure time, such as meson or sphinx. Since we are requiring that "meson" and "sphinx" are installed to the same Python environment as QEMU is configured to build with, this can produce some surprising failures when things are mismatched. This method is here to try and ease that sting by offering some actionable diagnosis. Signed-off-by: John Snow Message-Id: <20230511035435.734312-8-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 170 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index f6c4f37..2dbbc70 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -51,6 +51,8 @@ from importlib.util import find_spec import logging import os from pathlib import Path +import re +import shutil import site import subprocess import sys @@ -60,6 +62,7 @@ from typing import ( Any, Optional, Sequence, + Tuple, Union, ) import venv @@ -331,6 +334,128 @@ def make_venv( # pylint: disable=too-many-arguments print(builder.get_value("env_exe")) +def pkgname_from_depspec(dep_spec: str) -> str: + """ + Parse package name out of a PEP-508 depspec. + + See https://peps.python.org/pep-0508/#names + """ + match = re.match( + r"^([A-Z0-9]([A-Z0-9._-]*[A-Z0-9])?)", dep_spec, re.IGNORECASE + ) + if not match: + raise ValueError( + f"dep_spec '{dep_spec}'" + " does not appear to contain a valid package name" + ) + return match.group(0) + + +def diagnose( + dep_spec: str, + online: bool, + wheels_dir: Optional[Union[str, Path]], + prog: Optional[str], +) -> Tuple[str, bool]: + """ + Offer a summary to the user as to why a package failed to be installed. + + :param dep_spec: The package we tried to ensure, e.g. 'meson>=0.61.5' + :param online: Did we allow PyPI access? + :param prog: + Optionally, a shell program name that can be used as a + bellwether to detect if this program is installed elsewhere on + the system. This is used to offer advice when a program is + detected for a different python version. + :param wheels_dir: + Optionally, a directory that was searched for vendored packages. + """ + # pylint: disable=too-many-branches + + # Some errors are not particularly serious + bad = False + + pkg_name = pkgname_from_depspec(dep_spec) + pkg_version = None + + has_importlib = False + try: + # Python 3.8+ stdlib + # pylint: disable=import-outside-toplevel + # pylint: disable=no-name-in-module + # pylint: disable=import-error + from importlib.metadata import ( # type: ignore + PackageNotFoundError, + version, + ) + + has_importlib = True + try: + pkg_version = version(pkg_name) + except PackageNotFoundError: + pass + except ModuleNotFoundError: + pass + + lines = [] + + if pkg_version: + lines.append( + f"Python package '{pkg_name}' version '{pkg_version}' was found," + " but isn't suitable." + ) + elif has_importlib: + lines.append( + f"Python package '{pkg_name}' was not found nor installed." + ) + else: + lines.append( + f"Python package '{pkg_name}' is either not found or" + " not a suitable version." + ) + + if wheels_dir: + lines.append( + "No suitable version found in, or failed to install from" + f" '{wheels_dir}'." + ) + bad = True + + if online: + lines.append("A suitable version could not be obtained from PyPI.") + bad = True + else: + lines.append( + "mkvenv was configured to operate offline and did not check PyPI." + ) + + if prog and not pkg_version: + which = shutil.which(prog) + if which: + if sys.base_prefix in site.PREFIXES: + pypath = Path(sys.executable).resolve() + lines.append( + f"'{prog}' was detected on your system at '{which}', " + f"but the Python package '{pkg_name}' was not found by " + f"this Python interpreter ('{pypath}'). " + f"Typically this means that '{prog}' has been installed " + "against a different Python interpreter on your system." + ) + else: + lines.append( + f"'{prog}' was detected on your system at '{which}', " + "but the build is using an isolated virtual environment." + ) + bad = True + + lines = [f" • {line}" for line in lines] + if bad: + lines.insert(0, f"Could not provide build dependency '{dep_spec}':") + else: + lines.insert(0, f"'{dep_spec}' not found:") + return os.linesep.join(lines), bad + + def pip_install( args: Sequence[str], online: bool = False, @@ -364,7 +489,7 @@ def pip_install( ) -def ensure( +def _do_ensure( dep_specs: Sequence[str], online: bool = False, wheels_dir: Optional[Union[str, Path]] = None, @@ -402,6 +527,39 @@ def ensure( pip_install(args=absent, online=online, wheels_dir=wheels_dir) +def ensure( + dep_specs: Sequence[str], + online: bool = False, + wheels_dir: Optional[Union[str, Path]] = None, + prog: Optional[str] = None, +) -> None: + """ + Use pip to ensure we have the package specified by @dep_specs. + + If the package is already installed, do nothing. If online and + wheels_dir are both provided, prefer packages found in wheels_dir + first before connecting to PyPI. + + :param dep_specs: + PEP 508 dependency specifications. e.g. ['meson>=0.61.5']. + :param online: If True, fall back to PyPI. + :param wheels_dir: If specified, search this path for packages. + :param prog: + If specified, use this program name for error diagnostics that will + be presented to the user. e.g., 'sphinx-build' can be used as a + bellwether for the presence of 'sphinx'. + """ + print(f"mkvenv: checking for {', '.join(dep_specs)}", file=sys.stderr) + try: + _do_ensure(dep_specs, online, wheels_dir) + except subprocess.CalledProcessError as exc: + # Well, that's not good. + msg, bad = diagnose(dep_specs[0], online, wheels_dir, prog) + if bad: + raise Ouch(msg) from exc + raise SystemExit(f"\n{msg}\n\n") from exc + + def _add_create_subcommand(subparsers: Any) -> None: subparser = subparsers.add_parser("create", help="create a venv") subparser.add_argument( @@ -428,6 +586,15 @@ def _add_ensure_subcommand(subparsers: Any) -> None: help="Path to vendored packages where we may install from.", ) subparser.add_argument( + "--diagnose", + type=str, + action="store", + help=( + "Name of a shell utility to use for " + "diagnostics if this command fails." + ), + ) + subparser.add_argument( "dep_specs", type=str, action="store", @@ -476,6 +643,7 @@ def main() -> int: dep_specs=args.dep_specs, online=args.online, wheels_dir=args.dir, + prog=args.diagnose, ) logger.debug("mkvenv.py %s: exiting", args.command) except Ouch as exc: -- cgit v1.1 From 928348949d1d04f67715fa7125e7e1fa3ff40f7c Mon Sep 17 00:00:00 2001 From: John Snow Date: Tue, 16 May 2023 12:08:11 +0200 Subject: mkvenv: add console script entry point generation When creating a virtual environment that inherits system packages, script entry points (like "meson", "sphinx-build", etc) are not re-generated with the correct shebang. When you are *inside* of the venv, this is not a problem, but if you are *outside* of it, you will not have a script that engages the virtual environment appropriately. Add a mechanism that generates new entry points for pre-existing packages so that we can use these scripts to run "meson", "sphinx-build", "pip", unambiguously inside the venv. Signed-off-by: Paolo Bonzini Signed-off-by: John Snow Message-Id: <20230511035435.734312-9-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 114 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index 2dbbc70..f17c3d3 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -60,6 +60,7 @@ import sysconfig from types import SimpleNamespace from typing import ( Any, + Iterator, Optional, Sequence, Tuple, @@ -69,6 +70,7 @@ import venv import warnings import distlib.database +import distlib.scripts import distlib.version @@ -334,6 +336,113 @@ def make_venv( # pylint: disable=too-many-arguments print(builder.get_value("env_exe")) +def _gen_importlib(packages: Sequence[str]) -> Iterator[str]: + # pylint: disable=import-outside-toplevel + # pylint: disable=no-name-in-module + # pylint: disable=import-error + try: + # First preference: Python 3.8+ stdlib + from importlib.metadata import ( # type: ignore + PackageNotFoundError, + distribution, + ) + except ImportError as exc: + logger.debug("%s", str(exc)) + # Second preference: Commonly available PyPI backport + from importlib_metadata import ( # type: ignore + PackageNotFoundError, + distribution, + ) + + def _generator() -> Iterator[str]: + for package in packages: + try: + entry_points = distribution(package).entry_points + except PackageNotFoundError: + continue + + # The EntryPoints type is only available in 3.10+, + # treat this as a vanilla list and filter it ourselves. + entry_points = filter( + lambda ep: ep.group == "console_scripts", entry_points + ) + + for entry_point in entry_points: + yield f"{entry_point.name} = {entry_point.value}" + + return _generator() + + +def _gen_pkg_resources(packages: Sequence[str]) -> Iterator[str]: + # pylint: disable=import-outside-toplevel + # Bundled with setuptools; has a good chance of being available. + import pkg_resources + + def _generator() -> Iterator[str]: + for package in packages: + try: + eps = pkg_resources.get_entry_map(package, "console_scripts") + except pkg_resources.DistributionNotFound: + continue + + for entry_point in eps.values(): + yield str(entry_point) + + return _generator() + + +def generate_console_scripts( + packages: Sequence[str], + python_path: Optional[str] = None, + bin_path: Optional[str] = None, +) -> None: + """ + Generate script shims for console_script entry points in @packages. + """ + if python_path is None: + python_path = sys.executable + if bin_path is None: + bin_path = sysconfig.get_path("scripts") + assert bin_path is not None + + logger.debug( + "generate_console_scripts(packages=%s, python_path=%s, bin_path=%s)", + packages, + python_path, + bin_path, + ) + + if not packages: + return + + def _get_entry_points() -> Iterator[str]: + """Python 3.7 compatibility shim for iterating entry points.""" + # Python 3.8+, or Python 3.7 with importlib_metadata installed. + try: + return _gen_importlib(packages) + except ImportError as exc: + logger.debug("%s", str(exc)) + + # Python 3.7 with setuptools installed. + try: + return _gen_pkg_resources(packages) + except ImportError as exc: + logger.debug("%s", str(exc)) + raise Ouch( + "Neither importlib.metadata nor pkg_resources found, " + "can't generate console script shims.\n" + "Use Python 3.8+, or install importlib-metadata or setuptools." + ) from exc + + maker = distlib.scripts.ScriptMaker(None, bin_path) + maker.variants = {""} + maker.clobber = False + + for entry_point in _get_entry_points(): + for filename in maker.make(entry_point): + logger.debug("wrote console_script '%s'", filename) + + def pkgname_from_depspec(dep_spec: str) -> str: """ Parse package name out of a PEP-508 depspec. @@ -512,6 +621,7 @@ def _do_ensure( ) dist_path = distlib.database.DistributionPath(include_egg=True) absent = [] + present = [] for spec in dep_specs: matcher = distlib.version.LegacyMatcher(spec) dist = dist_path.get_distribution(matcher.name) @@ -519,6 +629,10 @@ def _do_ensure( absent.append(spec) else: logger.info("found %s", dist) + present.append(matcher.name) + + if present: + generate_console_scripts(present) if absent: # Some packages are missing or aren't a suitable version, -- cgit v1.1 From 68ea6d17fe531e383394573251359ab4f99f7091 Mon Sep 17 00:00:00 2001 From: John Snow Date: Tue, 16 May 2023 09:05:51 +0200 Subject: mkvenv: use pip's vendored distlib as a fallback distlib is usually not installed on Linux distribution, but it is vendored into pip. Because the virtual environment has pip via ensurepip, we can piggy-back on pip's vendored version. This could break if they move our cheese in the future, but the fix would be simply to require distlib. If it is debundled, as it is on msys, it is simply available directly. Signed-off-by: John Snow [Move to toplevel. - Paolo] Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index f17c3d3..fb91f92 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -69,10 +69,25 @@ from typing import ( import venv import warnings -import distlib.database -import distlib.scripts -import distlib.version +# Try to load distlib, with a fallback to pip's vendored version. +# HAVE_DISTLIB is checked below, just-in-time, so that mkvenv does not fail +# outside the venv or before a potential call to ensurepip in checkpip(). +HAVE_DISTLIB = True +try: + import distlib.database + import distlib.scripts + import distlib.version +except ImportError: + try: + # Reach into pip's cookie jar. pylint and flake8 don't understand + # that these imports will be used via distlib.xxx. + from pip._vendor import distlib + import pip._vendor.distlib.database # noqa, pylint: disable=unused-import + import pip._vendor.distlib.scripts # noqa, pylint: disable=unused-import + import pip._vendor.distlib.version # noqa, pylint: disable=unused-import + except ImportError: + HAVE_DISTLIB = False # Do not add any mandatory dependencies from outside the stdlib: # This script *must* be usable standalone! @@ -664,6 +679,10 @@ def ensure( bellwether for the presence of 'sphinx'. """ print(f"mkvenv: checking for {', '.join(dep_specs)}", file=sys.stderr) + + if not HAVE_DISTLIB: + raise Ouch("a usable distlib could not be found, please install it") + try: _do_ensure(dep_specs, online, wheels_dir) except subprocess.CalledProcessError as exc: -- cgit v1.1 From f1ad527ff5f789a19c79f5f39a87f7a8f78d81b9 Mon Sep 17 00:00:00 2001 From: John Snow Date: Tue, 16 May 2023 09:18:23 +0200 Subject: mkvenv: avoid ensurepip if pip is installed Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 67 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index fb91f92..a9c5660 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -11,6 +11,8 @@ options: Commands: command Description create create a venv + post_init + post-venv initialization ensure Ensure that the specified package is installed. -------------------------------------------------- @@ -25,6 +27,13 @@ options: -------------------------------------------------- +usage: mkvenv post_init [-h] + +options: + -h, --help show this help message and exit + +-------------------------------------------------- + usage: mkvenv ensure [-h] [--online] [--dir DIR] dep_spec... positional arguments: @@ -111,7 +120,9 @@ class QemuEnvBuilder(venv.EnvBuilder): The primary difference is that it emulates a "nested" virtual environment when invoked from inside of an existing virtual - environment by including packages from the parent. + environment by including packages from the parent. Also, + "ensurepip" is replaced if possible with just recreating pip's + console_scripts inside the virtual environment. Parameters for base class init: - system_site_packages: bool = False @@ -138,8 +149,19 @@ class QemuEnvBuilder(venv.EnvBuilder): # The venv we are currently in, also does so. kwargs["system_site_packages"] = sys.base_prefix in site.PREFIXES - if kwargs.get("with_pip", False): - check_ensurepip() + # ensurepip is slow: venv creation can be very fast for cases where + # we allow the use of system_site_packages. Therefore, ensurepip is + # replaced with our own script generation once the virtual environment + # is setup. + self.want_pip = kwargs.get("with_pip", False) + if self.want_pip: + if ( + kwargs.get("system_site_packages", False) + and not need_ensurepip() + ): + kwargs["with_pip"] = False + else: + check_ensurepip() super().__init__(*args, **kwargs) @@ -211,6 +233,14 @@ class QemuEnvBuilder(venv.EnvBuilder): with open(pth_file, "w", encoding="UTF-8") as file: file.write(parent_libpath + os.linesep) + if self.want_pip: + args = [ + context.env_exe, + __file__, + "post_init", + ] + subprocess.run(args, check=True) + def get_value(self, field: str) -> str: """ Get a string value from the context namespace after a call to build. @@ -223,6 +253,19 @@ class QemuEnvBuilder(venv.EnvBuilder): return ret +def need_ensurepip() -> bool: + """ + Tests for the presence of setuptools and pip. + + :return: `True` if we do not detect both packages. + """ + # Don't try to actually import them, it's fraught with danger: + # https://github.com/pypa/setuptools/issues/2993 + if find_spec("setuptools") and find_spec("pip"): + return False + return True + + def check_ensurepip() -> None: """ Check that we have ensurepip. @@ -693,6 +736,17 @@ def ensure( raise SystemExit(f"\n{msg}\n\n") from exc +def post_venv_setup() -> None: + """ + This is intended to be run *inside the venv* after it is created. + """ + logger.debug("post_venv_setup()") + # Generate a 'pip' script so the venv is usable in a normal + # way from the CLI. This only happens when we inherited pip from a + # parent/system-site and haven't run ensurepip in some way. + generate_console_scripts(["pip"]) + + def _add_create_subcommand(subparsers: Any) -> None: subparser = subparsers.add_parser("create", help="create a venv") subparser.add_argument( @@ -703,6 +757,10 @@ def _add_create_subcommand(subparsers: Any) -> None: ) +def _add_post_init_subcommand(subparsers: Any) -> None: + subparsers.add_parser("post_init", help="post-venv initialization") + + def _add_ensure_subcommand(subparsers: Any) -> None: subparser = subparsers.add_parser( "ensure", help="Ensure that the specified package is installed." @@ -761,6 +819,7 @@ def main() -> int: ) _add_create_subcommand(subparsers) + _add_post_init_subcommand(subparsers) _add_ensure_subcommand(subparsers) args = parser.parse_args() @@ -771,6 +830,8 @@ def main() -> int: system_site_packages=True, clear=True, ) + if args.command == "post_init": + post_venv_setup() if args.command == "ensure": ensure( dep_specs=args.dep_specs, -- cgit v1.1 From c804962617dc987e202f8e9ce0210503fd489616 Mon Sep 17 00:00:00 2001 From: John Snow Date: Tue, 16 May 2023 09:13:32 +0200 Subject: mkvenv: work around broken pip installations on Debian 10 This is a workaround intended for Debian 10, where the debian-patched pip does not function correctly if accessed from within a virtual environment. We don't support Debian 10 as a build platform any longer, though we do still utilize it for our build-tricore-softmmu CI test. It's also possible that this bug might appear on other derivative platforms and this workaround may prove useful. Signed-off-by: Paolo Bonzini Signed-off-by: John Snow Message-Id: <20230511035435.734312-11-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 72 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 16 deletions(-) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index a9c5660..5586199 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -161,7 +161,7 @@ class QemuEnvBuilder(venv.EnvBuilder): ): kwargs["with_pip"] = False else: - check_ensurepip() + check_ensurepip(suggest_remedy=True) super().__init__(*args, **kwargs) @@ -266,7 +266,7 @@ def need_ensurepip() -> bool: return True -def check_ensurepip() -> None: +def check_ensurepip(prefix: str = "", suggest_remedy: bool = False) -> None: """ Check that we have ensurepip. @@ -277,12 +277,15 @@ def check_ensurepip() -> None: "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.)" + "(Debian puts ensurepip in its python3-venv package.)\n" ) - raise Ouch(msg) + if suggest_remedy: + msg += ( + "Either install ensurepip, or alleviate the need for it in the" + " first place by installing pip and setuptools for " + f"'{sys.executable}'.\n" + ) + raise Ouch(prefix + msg) # ensurepip uses pyexpat, which can also go missing on us: if not find_spec("pyexpat"): @@ -290,12 +293,15 @@ def check_ensurepip() -> None: "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'.)" + "(NetBSD's pkgsrc debundles this to e.g. 'py310-expat'.)\n" ) - raise Ouch(msg) + if suggest_remedy: + msg += ( + "Either install pyexpat, or alleviate the need for it in the " + "first place by installing pip and setuptools for " + f"'{sys.executable}'.\n" + ) + raise Ouch(prefix + msg) def make_venv( # pylint: disable=too-many-arguments @@ -501,6 +507,38 @@ def generate_console_scripts( logger.debug("wrote console_script '%s'", filename) +def checkpip() -> bool: + """ + Debian10 has a pip that's broken when used inside of a virtual environment. + + We try to detect and correct that case here. + """ + try: + # pylint: disable=import-outside-toplevel,unused-import,import-error + # pylint: disable=redefined-outer-name + import pip._internal # type: ignore # noqa: F401 + + logger.debug("pip appears to be working correctly.") + return False + except ModuleNotFoundError as exc: + if exc.name == "pip._internal": + # Uh, fair enough. They did say "internal". + # Let's just assume it's fine. + return False + logger.warning("pip appears to be malfunctioning: %s", str(exc)) + + check_ensurepip("pip appears to be non-functional, and ") + + logger.debug("Attempting to repair pip ...") + subprocess.run( + (sys.executable, "-m", "ensurepip"), + stdout=subprocess.DEVNULL, + check=True, + ) + logger.debug("Pip is now (hopefully) repaired!") + return True + + def pkgname_from_depspec(dep_spec: str) -> str: """ Parse package name out of a PEP-508 depspec. @@ -741,10 +779,12 @@ def post_venv_setup() -> None: This is intended to be run *inside the venv* after it is created. """ logger.debug("post_venv_setup()") - # Generate a 'pip' script so the venv is usable in a normal - # way from the CLI. This only happens when we inherited pip from a - # parent/system-site and haven't run ensurepip in some way. - generate_console_scripts(["pip"]) + # Test for a broken pip (Debian 10 or derivative?) and fix it if needed + if not checkpip(): + # Finally, generate a 'pip' script so the venv is usable in a normal + # way from the CLI. This only happens when we inherited pip from a + # parent/system-site and haven't run ensurepip in some way. + generate_console_scripts(["pip"]) def _add_create_subcommand(subparsers: Any) -> None: -- cgit v1.1 From 2274817f6c499fd31081d7973b7cbfdca17c44a8 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 10 May 2023 23:54:22 -0400 Subject: python: add vendor.py utility This is a teeny-tiny script that just downloads any packages we want to vendor from PyPI and stores them in qemu.git/python/wheels/. If I'm hit by a meteor, it'll be easy to replicate what I have done in order to udpate the vendored source. We don't really care which python runs it; it exists as a meta-utility with no external dependencies and we won't package or install it. It will be monitored by the linters/type checkers, though; so it's guaranteed safe on python 3.6+. Signed-off-by: John Snow Message-Id: <20230511035435.734312-15-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/vendor.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100755 python/scripts/vendor.py (limited to 'python/scripts') diff --git a/python/scripts/vendor.py b/python/scripts/vendor.py new file mode 100755 index 0000000..2370843 --- /dev/null +++ b/python/scripts/vendor.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +vendor - QEMU python vendoring utility + +usage: vendor [-h] + +QEMU python vendoring utility + +options: + -h, --help show this help message and exit +""" + +# Copyright (C) 2023 Red Hat, Inc. +# +# Authors: +# John Snow +# +# 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 +import os +from pathlib import Path +import subprocess +import sys +import tempfile + + +def main() -> int: + """Run the vendoring utility. See module-level docstring.""" + loud = False + if os.environ.get("DEBUG") or os.environ.get("V"): + loud = True + + # No options or anything for now, but I guess + # you'll figure that out when you run --help. + parser = argparse.ArgumentParser( + prog="vendor", + description="QEMU python vendoring utility", + ) + parser.parse_args() + + packages = { + "meson==0.61.5": + "58c2ddb5f885da0e929f15d89f38d8a7d97f981f56815bcba008414f8511f59a", + } + + vendor_dir = Path(__file__, "..", "..", "wheels").resolve() + + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as file: + for dep_spec, checksum in packages.items(): + file.write(f"{dep_spec} --hash=sha256:{checksum}") + file.flush() + + cli_args = [ + "pip", + "download", + "--dest", + str(vendor_dir), + "--require-hashes", + "-r", + file.name, + ] + if loud: + cli_args.append("-v") + + print(" ".join(cli_args)) + subprocess.run(cli_args, check=True) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.1 From 02312f1af1524253d1b98f4fa6dfe23dc53f1c02 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 10 May 2023 23:54:33 -0400 Subject: mkvenv: mark command as required This is only available in Python 3.7+. Signed-off-by: Paolo Bonzini Signed-off-by: John Snow Message-Id: <20230511035435.734312-26-jsnow@redhat.com> Signed-off-by: Paolo Bonzini --- python/scripts/mkvenv.py | 1 + 1 file changed, 1 insertion(+) (limited to 'python/scripts') diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py index 5586199..8c036c0 100644 --- a/python/scripts/mkvenv.py +++ b/python/scripts/mkvenv.py @@ -854,6 +854,7 @@ def main() -> int: subparsers = parser.add_subparsers( title="Commands", dest="command", + required=True, metavar="command", help="Description", ) -- cgit v1.1 From 3b087f79a48807f348ea61469175e66b28ba44de Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Thu, 23 Dec 2021 15:29:56 +0100 Subject: meson: require 0.63.0 This version allows cleanups in modinfo collection, but they only work with Ninja 1.9.x and 1.8.x is still supported. It also supports the equivalent of QEMU's --static option to configure. The wheel file is bumped to 0.63.3, the last release in the 0.63 branch. Signed-off-by: Paolo Bonzini --- python/scripts/vendor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'python/scripts') diff --git a/python/scripts/vendor.py b/python/scripts/vendor.py index 2370843..34486a5 100755 --- a/python/scripts/vendor.py +++ b/python/scripts/vendor.py @@ -41,8 +41,8 @@ def main() -> int: parser.parse_args() packages = { - "meson==0.61.5": - "58c2ddb5f885da0e929f15d89f38d8a7d97f981f56815bcba008414f8511f59a", + "meson==0.63.3": + "d677b809c4895dcbaac9bf6c43703fcb3609a4b24c6057c78f828590049cf43a", } vendor_dir = Path(__file__, "..", "..", "wheels").resolve() -- cgit v1.1