From: John Snow <jsnow@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Warner Losh" <imp@bsdimp.com>,
"Peter Maydell" <peter.maydell@linaro.org>,
"Daniel P. Berrangé" <berrange@redhat.com>,
"Ani Sinha" <anisinha@redhat.com>,
"Beraldo Leal" <bleal@redhat.com>,
"Markus Armbruster" <armbru@redhat.com>,
"Ryo ONODERA" <ryoon@netbsd.org>,
"Paolo Bonzini" <pbonzini@redhat.com>,
"Kyle Evans" <kevans@freebsd.org>,
"Alex Bennée" <alex.bennee@linaro.org>,
"Michael Roth" <michael.roth@amd.com>,
"Reinoud Zandijk" <reinoud@netbsd.org>,
"Marc-André Lureau" <marcandre.lureau@redhat.com>,
"Cleber Rosa" <crosa@redhat.com>,
"Thomas Huth" <thuth@redhat.com>,
"Michael S. Tsirkin" <mst@redhat.com>,
"John Snow" <jsnow@redhat.com>,
"Philippe Mathieu-Daudé" <philmd@linaro.org>,
"Wainer dos Santos Moschetta" <wainersm@redhat.com>
Subject: [PATCH 03/27] python: add mkvenv.py
Date: Wed, 10 May 2023 23:54:11 -0400 [thread overview]
Message-ID: <20230511035435.734312-4-jsnow@redhat.com> (raw)
In-Reply-To: <20230511035435.734312-1-jsnow@redhat.com>
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 <pbonzini@redhat.com>
Signed-off-by: John Snow <jsnow@redhat.com>
---
python/scripts/mkvenv.py | 289 +++++++++++++++++++++++++++++++++++++++
python/setup.cfg | 9 ++
python/tests/flake8.sh | 1 +
python/tests/isort.sh | 1 +
python/tests/mypy.sh | 1 +
python/tests/pylint.sh | 1 +
6 files changed, 302 insertions(+)
create mode 100644 python/scripts/mkvenv.py
diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py
new file mode 100644
index 0000000000..ce52b55976
--- /dev/null
+++ b/python/scripts/mkvenv.py
@@ -0,0 +1,289 @@
+"""
+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__(...)")
+ 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 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(with_pip: bool) -> None:
+ """
+ Check that we have ensurepip.
+
+ Raise a fatal exception with a helpful hint if it isn't available.
+ """
+ if with_pip and 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)
+
+
+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: Optional[bool] = None,
+) -> 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 run "ensurepip" or not. If unspecified, this will
+ default to False if system_site_packages is True and a usable
+ version of pip is found.
+ """
+ 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,
+ )
+
+ # ensurepip is slow: venv creation can be very fast for cases where
+ # we allow the use of system_site_packages. Toggle ensure_pip on only
+ # in the cases where we really need it.
+ if with_pip is None:
+ with_pip = True if not system_site_packages else need_ensurepip()
+ logger.debug("with_pip unset, choosing %s", with_pip)
+
+ check_ensurepip(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: # pylint: disable=bare-except
+ logger.exception("mkvenv did not complete successfully:")
+ return 2
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/python/setup.cfg b/python/setup.cfg
index 3f36502954..5b25f810fa 100644
--- a/python/setup.cfg
+++ b/python/setup.cfg
@@ -103,6 +103,15 @@ ignore_missing_imports = True
[mypy-pygments]
ignore_missing_imports = True
+[mypy-importlib.metadata]
+ignore_missing_imports = True
+
+[mypy-importlib_metadata]
+ignore_missing_imports = True
+
+[mypy-pkg_resources]
+ignore_missing_imports = True
+
[pylint.messages control]
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
diff --git a/python/tests/flake8.sh b/python/tests/flake8.sh
index 1cd7d40fad..e013699645 100755
--- a/python/tests/flake8.sh
+++ b/python/tests/flake8.sh
@@ -1,2 +1,3 @@
#!/bin/sh -e
python3 -m flake8 qemu/
+python3 -m flake8 scripts/
diff --git a/python/tests/isort.sh b/python/tests/isort.sh
index 4480405bfb..66c2f7df0f 100755
--- a/python/tests/isort.sh
+++ b/python/tests/isort.sh
@@ -1,2 +1,3 @@
#!/bin/sh -e
python3 -m isort -c qemu/
+python3 -m isort -c scripts/
diff --git a/python/tests/mypy.sh b/python/tests/mypy.sh
index 5f980f563b..a33a3f58ab 100755
--- a/python/tests/mypy.sh
+++ b/python/tests/mypy.sh
@@ -1,2 +1,3 @@
#!/bin/sh -e
python3 -m mypy -p qemu
+python3 -m mypy scripts/
diff --git a/python/tests/pylint.sh b/python/tests/pylint.sh
index 03d64705a1..2b68da90df 100755
--- a/python/tests/pylint.sh
+++ b/python/tests/pylint.sh
@@ -1,3 +1,4 @@
#!/bin/sh -e
# See commit message for environment variable explainer.
SETUPTOOLS_USE_DISTUTILS=stdlib python3 -m pylint qemu/
+SETUPTOOLS_USE_DISTUTILS=stdlib python3 -m pylint scripts/
--
2.40.0
next prev parent reply other threads:[~2023-05-11 3:56 UTC|newest]
Thread overview: 37+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-05-11 3:54 [PATCH 00/27] configure: create a python venv and ensure meson, sphinx John Snow
2023-05-11 3:54 ` [PATCH 01/27] python: shut up "pip install" during "make check-minreqs" John Snow
2023-05-11 3:54 ` [PATCH 02/27] python: update pylint configuration John Snow
2023-05-11 3:54 ` John Snow [this message]
2023-05-11 3:54 ` [PATCH 04/27] mkvenv: add better error message for missing pyexpat module John Snow
2023-05-11 3:54 ` [PATCH 05/27] mkvenv: add nested venv workaround John Snow
2023-05-11 3:54 ` [PATCH 06/27] mkvenv: add ensure subcommand John Snow
2023-05-11 3:54 ` [PATCH 07/27] mkvenv: add diagnose() method for ensure() failures John Snow
2023-05-11 6:53 ` Paolo Bonzini
2023-05-11 15:53 ` John Snow
2023-05-11 15:56 ` Paolo Bonzini
2023-05-11 15:59 ` John Snow
2023-05-11 3:54 ` [PATCH 08/27] mkvenv: add console script entry point generation John Snow
2023-05-11 3:54 ` [PATCH 09/27] mkvenv: create pip binary in virtual environment John Snow
2023-05-11 3:54 ` [PATCH 10/27] mkvenv: work around broken pip installations on Debian 10 John Snow
2023-05-11 3:54 ` [PATCH 11/27] tests/docker: add python3-venv dependency John Snow
2023-05-11 3:54 ` [PATCH 12/27] tests/vm: Configure netbsd to use Python 3.10 John Snow
2023-05-11 3:54 ` [PATCH 13/27] tests/vm: add py310-expat to NetBSD John Snow
2023-05-11 3:54 ` [PATCH 14/27] python: add vendor.py utility John Snow
2023-05-11 3:54 ` [PATCH 15/27] configure: create a python venv unconditionally John Snow
2023-05-11 3:54 ` [PATCH 16/27] python/wheels: add vendored meson package John Snow
2023-05-11 3:54 ` [PATCH 17/27] configure: use 'mkvenv ensure meson' to bootstrap meson John Snow
2023-05-11 3:54 ` [PATCH 18/27] qemu.git: drop meson git submodule John Snow
2023-05-11 3:54 ` [PATCH 19/27] tests: Use configure-provided pyvenv for tests John Snow
2023-05-11 3:54 ` [PATCH 20/27] configure: move --enable-docs and --disable-docs back to configure John Snow
2023-05-11 3:54 ` [PATCH 21/27] configure: bootstrap sphinx with mkvenv John Snow
2023-05-11 3:54 ` [PATCH 22/27] configure: add --enable-pypi and --disable-pypi John Snow
2023-05-11 3:54 ` [PATCH 23/27] Python: Drop support for Python 3.6 John Snow
2023-05-11 3:54 ` [PATCH 24/27] configure: Add courtesy hint to Python version failure message John Snow
2023-05-11 3:54 ` [PATCH 25/27] mkvenv: mark command as required John Snow
2023-05-11 3:54 ` [PATCH 26/27] python: bump some of the dependencies John Snow
2023-05-11 3:54 ` [PATCH 27/27] mkvenv.py: experiment; use distlib to generate script entry points John Snow
2023-05-11 6:57 ` Paolo Bonzini
2023-05-11 7:02 ` Paolo Bonzini
2023-05-11 15:58 ` John Snow
2023-05-11 16:14 ` Paolo Bonzini
2023-05-11 16:16 ` John Snow
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20230511035435.734312-4-jsnow@redhat.com \
--to=jsnow@redhat.com \
--cc=alex.bennee@linaro.org \
--cc=anisinha@redhat.com \
--cc=armbru@redhat.com \
--cc=berrange@redhat.com \
--cc=bleal@redhat.com \
--cc=crosa@redhat.com \
--cc=imp@bsdimp.com \
--cc=kevans@freebsd.org \
--cc=marcandre.lureau@redhat.com \
--cc=michael.roth@amd.com \
--cc=mst@redhat.com \
--cc=pbonzini@redhat.com \
--cc=peter.maydell@linaro.org \
--cc=philmd@linaro.org \
--cc=qemu-devel@nongnu.org \
--cc=reinoud@netbsd.org \
--cc=ryoon@netbsd.org \
--cc=thuth@redhat.com \
--cc=wainersm@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).