All of lore.kernel.org
 help / color / mirror / Atom feed
From: John Snow <jsnow@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Warner Losh" <imp@bsdimp.com>, "Beraldo Leal" <bleal@redhat.com>,
	"John Snow" <jsnow@redhat.com>, "Kyle Evans" <kevans@freebsd.org>,
	"Paolo Bonzini" <pbonzini@redhat.com>,
	"Thomas Huth" <thuth@redhat.com>,
	"Daniel Berrange" <berrange@redhat.com>,
	"Reinoud Zandijk" <reinoud@netbsd.org>,
	"Wainer dos Santos Moschetta" <wainersm@redhat.com>,
	"Cleber Rosa" <crosa@redhat.com>,
	"Ryo ONODERA" <ryoon@netbsd.org>,
	"Philippe Mathieu-Daudé" <philmd@linaro.org>,
	"Ani Sinha" <ani@anisinha.ca>,
	"Michael S. Tsirkin" <mst@redhat.com>,
	"Alex Bennée" <alex.bennee@linaro.org>
Subject: [RFC PATCH v3 03/20] mkvenv: add console script entry point generation
Date: Mon, 24 Apr 2023 16:02:31 -0400	[thread overview]
Message-ID: <20230424200248.1183394-4-jsnow@redhat.com> (raw)
In-Reply-To: <20230424200248.1183394-1-jsnow@redhat.com>

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: John Snow <jsnow@redhat.com>
---
 python/scripts/mkvenv.py | 179 +++++++++++++++++++++++++++++++++++++--
 1 file changed, 172 insertions(+), 7 deletions(-)

diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py
index 1dfcc0198a..f355cb54fb 100644
--- a/python/scripts/mkvenv.py
+++ b/python/scripts/mkvenv.py
@@ -14,13 +14,14 @@
 
 --------------------------------------------------
 
-usage: mkvenv create [-h] target
+usage: mkvenv create [-h] [--gen GEN] target
 
 positional arguments:
   target      Target directory to install virtual environment into.
 
 options:
   -h, --help  show this help message and exit
+  --gen GEN   Regenerate console_scripts for given packages, if found.
 
 """
 
@@ -38,11 +39,20 @@
 import logging
 import os
 from pathlib import Path
+import re
+import stat
 import subprocess
 import sys
 import traceback
 from types import SimpleNamespace
-from typing import Any, Optional, Union
+from typing import (
+    Any,
+    Dict,
+    Iterator,
+    Optional,
+    Sequence,
+    Union,
+)
 import venv
 
 
@@ -60,10 +70,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 only functional change is that it adds the ability to regenerate
+    console_script shims for packages available via system_site
+    packages.
 
     Parameters for base class init:
       - system_site_packages: bool = False
@@ -77,6 +86,7 @@ class QemuEnvBuilder(venv.EnvBuilder):
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
         logger.debug("QemuEnvBuilder.__init__(...)")
+        self.script_packages = kwargs.pop("script_packages", ())
         super().__init__(*args, **kwargs)
 
         # The EnvBuilder class is cute and toggles this setting off
@@ -87,6 +97,12 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
     def post_setup(self, context: SimpleNamespace) -> None:
         logger.debug("post_setup(...)")
 
+        # Generate console_script entry points for system packages:
+        if self._system_site_packages:
+            generate_console_scripts(
+                context.env_exe, context.bin_path, self.script_packages
+            )
+
         # print the python executable to stdout for configure.
         print(context.env_exe)
 
@@ -129,6 +145,7 @@ def make_venv(  # pylint: disable=too-many-arguments
     clear: bool = True,
     symlinks: Optional[bool] = None,
     with_pip: Optional[bool] = None,
+    script_packages: Sequence[str] = (),
 ) -> None:
     """
     Create a venv using `QemuEnvBuilder`.
@@ -149,16 +166,20 @@ def make_venv(  # pylint: disable=too-many-arguments
         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.
+    :param script_packages:
+        A sequence of package names to generate console entry point
+        shims for, when system_site_packages is True.
     """
     logging.debug(
         "%s: make_venv(env_dir=%s, system_site_packages=%s, "
-        "clear=%s, symlinks=%s, with_pip=%s)",
+        "clear=%s, symlinks=%s, with_pip=%s, script_packages=%s)",
         __file__,
         str(env_dir),
         system_site_packages,
         clear,
         symlinks,
         with_pip,
+        script_packages,
     )
 
     print(f"MKVENV {str(env_dir)}", file=sys.stderr)
@@ -181,6 +202,7 @@ def make_venv(  # pylint: disable=too-many-arguments
         clear=clear,
         symlinks=symlinks,
         with_pip=with_pip,
+        script_packages=script_packages,
     )
     try:
         logger.debug("Invoking builder.create()")
@@ -221,8 +243,147 @@ def _stringify(data: Optional[Union[str, bytes]]) -> Optional[str]:
         raise Ouch("VENV creation subprocess failed.") from exc
 
 
+def _gen_importlib(packages: Sequence[str]) -> Iterator[Dict[str, str]]:
+    # pylint: disable=import-outside-toplevel
+    try:
+        # First preference: Python 3.8+ stdlib
+        from importlib.metadata import (
+            PackageNotFoundError,
+            distribution,
+        )
+    except ImportError as exc:
+        logger.debug("%s", str(exc))
+        # Second preference: Commonly available PyPI backport
+        from importlib_metadata import (
+            PackageNotFoundError,
+            distribution,
+        )
+
+    # Borrowed from CPython (Lib/importlib/metadata/__init__.py)
+    pattern = re.compile(
+        r"(?P<module>[\w.]+)\s*"
+        r"(:\s*(?P<attr>[\w.]+)\s*)?"
+        r"((?P<extras>\[.*\])\s*)?$"
+    )
+
+    def _generator() -> Iterator[Dict[str, 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:
+                # Python 3.8 doesn't have 'module' or 'attr' attributes
+                if not (
+                    hasattr(entry_point, "module")
+                    and hasattr(entry_point, "attr")
+                ):
+                    match = pattern.match(entry_point.value)
+                    assert match is not None
+                    module = match.group("module")
+                    attr = match.group("attr")
+                else:
+                    module = entry_point.module
+                    attr = entry_point.attr
+                yield {
+                    "name": entry_point.name,
+                    "module": module,
+                    "import_name": attr,
+                    "func": attr,
+                }
+
+    return _generator()
+
+
+def _gen_pkg_resources(packages: Sequence[str]) -> Iterator[Dict[str, str]]:
+    # pylint: disable=import-outside-toplevel
+    # Bundled with setuptools; has a good chance of being available.
+    import pkg_resources
+
+    def _generator() -> Iterator[Dict[str, 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 {
+                    "name": entry_point.name,
+                    "module": entry_point.module_name,
+                    "import_name": ".".join(entry_point.attrs),
+                    "func": ".".join(entry_point.attrs),
+                }
+
+    return _generator()
+
+
+# Borrowed/adapted from pip's vendored version of distutils:
+SCRIPT_TEMPLATE = r"""#!{python_path:s}
+# -*- coding: utf-8 -*-
+import re
+import sys
+from {module:s} import {import_name:s}
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit({func:s}())
+"""
+
+
+def generate_console_scripts(
+    python_path: str, bin_path: str, packages: Sequence[str]
+) -> None:
+    """
+    Generate script shims for console_script entry points in @packages.
+    """
+    if not packages:
+        return
+
+    def _get_entry_points() -> Iterator[Dict[str, 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
+
+    for entry_point in _get_entry_points():
+        script_path = os.path.join(bin_path, entry_point["name"])
+        script = SCRIPT_TEMPLATE.format(python_path=python_path, **entry_point)
+        with open(script_path, "w", encoding="UTF-8") as file:
+            file.write(script)
+        mode = os.stat(script_path).st_mode | stat.S_IEXEC
+        os.chmod(script_path, mode)
+
+        logger.debug("wrote '%s'", script_path)
+
+
 def _add_create_subcommand(subparsers: Any) -> None:
     subparser = subparsers.add_parser("create", help="create a venv")
+    subparser.add_argument(
+        "--gen",
+        type=str,
+        action="append",
+        help="Regenerate console_scripts for given packages, if found.",
+    )
     subparser.add_argument(
         "target",
         type=str,
@@ -256,10 +417,14 @@ def main() -> int:
     args = parser.parse_args()
     try:
         if args.command == "create":
+            script_packages = []
+            for element in args.gen or ():
+                script_packages.extend(element.split(","))
             make_venv(
                 args.target,
                 system_site_packages=True,
                 clear=True,
+                script_packages=script_packages,
             )
         logger.debug("mkvenv.py %s: exiting", args.command)
     except Ouch as exc:
-- 
2.39.2



  parent reply	other threads:[~2023-04-24 20:09 UTC|newest]

Thread overview: 46+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-04-24 20:02 [RFC PATCH v3 00/20] configure: create a python venv and ensure meson, sphinx John Snow
2023-04-24 20:02 ` [RFC PATCH v3 01/20] python: update pylint configuration John Snow
2023-04-25 16:38   ` Daniel P. Berrangé
2023-04-24 20:02 ` [RFC PATCH v3 02/20] python: add mkvenv.py John Snow
2023-04-24 20:02 ` John Snow [this message]
2023-04-24 20:02 ` [RFC PATCH v3 04/20] mkvenv: Add better error message for missing pyexpat module John Snow
2023-04-24 20:02 ` [RFC PATCH v3 05/20] mkvenv: generate console entry shims from inside the venv John Snow
2023-04-24 20:02 ` [RFC PATCH v3 06/20] mkvenv: work around broken pip installations on Debian 10 John Snow
2023-04-24 20:02 ` [RFC PATCH v3 07/20] mkvenv: add nested venv workaround John Snow
2023-04-24 20:02 ` [RFC PATCH v3 08/20] mkvenv: add ensure subcommand John Snow
2023-04-24 20:02 ` [RFC PATCH v3 09/20] tests/docker: add python3-venv dependency John Snow
2023-04-25 16:42   ` Daniel P. Berrangé
2023-04-24 20:02 ` [RFC PATCH v3 10/20] tests/vm: Configure netbsd to use Python 3.10 John Snow
2023-04-25 16:43   ` Daniel P. Berrangé
2023-04-24 20:02 ` [RFC PATCH v3 11/20] tests/vm: add py310-expat to NetBSD John Snow
2023-04-25 16:45   ` Daniel P. Berrangé
2023-04-25 16:57     ` John Snow
2023-04-24 20:02 ` [RFC PATCH v3 12/20] scripts/make-release: download meson==0.61.5 .whl John Snow
2023-04-24 20:02 ` [RFC PATCH v3 13/20] configure: create a python venv unconditionally John Snow
2023-04-24 20:02 ` [RFC PATCH v3 14/20] configure: use 'mkvenv ensure meson' to bootstrap meson John Snow
2023-04-24 20:35   ` Warner Losh
2023-04-24 20:41     ` John Snow
2023-04-24 21:20       ` Warner Losh
2023-04-24 20:02 ` [RFC PATCH v3 15/20] configure: add --enable-pypi and --disable-pypi John Snow
2023-04-24 20:02 ` [RFC PATCH v3 16/20] tests: Use configure-provided pyvenv for tests John Snow
2023-04-24 20:02 ` [RFC PATCH v3 17/20] configure: move --enable-docs and --disable-docs back to configure John Snow
2023-04-24 20:02 ` [RFC PATCH v3 18/20] mkvenv: add diagnose() method for ensure() failures John Snow
2023-04-24 20:02 ` [RFC PATCH v3 19/20] configure: use --diagnose option with meson ensure John Snow
2023-04-24 20:02 ` [RFC PATCH v3 20/20] configure: bootstrap sphinx with mkvenv John Snow
2023-04-25 17:17 ` [RFC PATCH v3 00/20] configure: create a python venv and ensure meson, sphinx Daniel P. Berrangé
2023-04-25 17:22   ` John Snow
2023-04-25 17:34     ` John Snow
2023-04-25 18:10       ` Daniel P. Berrangé
2023-04-25 18:58         ` John Snow
2023-04-26  8:21           ` Daniel P. Berrangé
2023-04-26  8:35             ` Paolo Bonzini
2023-04-25 18:03     ` Daniel P. Berrangé
2023-04-26  8:05 ` Paolo Bonzini
2023-04-26  8:49   ` Paolo Bonzini
2023-04-26 16:16     ` John Snow
2023-04-26 19:10       ` Paolo Bonzini
2023-04-26  8:53 ` Daniel P. Berrangé
2023-04-26  9:08   ` Paolo Bonzini
2023-04-26 16:32     ` John Snow
2023-04-26 19:23       ` Paolo Bonzini
2023-05-01 19:20 ` 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=20230424200248.1183394-4-jsnow@redhat.com \
    --to=jsnow@redhat.com \
    --cc=alex.bennee@linaro.org \
    --cc=ani@anisinha.ca \
    --cc=berrange@redhat.com \
    --cc=bleal@redhat.com \
    --cc=crosa@redhat.com \
    --cc=imp@bsdimp.com \
    --cc=kevans@freebsd.org \
    --cc=mst@redhat.com \
    --cc=pbonzini@redhat.com \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.