qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
From: John Snow <jsnow@redhat.com>
To: Paolo Bonzini <pbonzini@redhat.com>
Cc: qemu-devel@nongnu.org, philmd@linaro.org, berrange@redhat.com
Subject: Re: [PATCH v2 07/27] mkvenv: add --diagnose option to explain "ensure" failures
Date: Tue, 16 May 2023 14:27:01 -0400	[thread overview]
Message-ID: <CAFn=p-be3oyTDFszUH24dOGw2EqbfRyTPxxvq+v3h=nGZVEMKA@mail.gmail.com> (raw)
In-Reply-To: <20230516105908.527838-7-pbonzini@redhat.com>

On Tue, May 16, 2023 at 6:59 AM Paolo Bonzini <pbonzini@redhat.com> wrote:
>
> From: John Snow <jsnow@redhat.com>
>
> 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 <jsnow@redhat.com>
> Message-Id: <20230511035435.734312-8-jsnow@redhat.com>
> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
> ---
>  python/scripts/mkvenv.py | 170 ++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 169 insertions(+), 1 deletion(-)
>
> diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py
> index fd4b62c70ffa..5ac22f584fab 100644
> --- a/python/scripts/mkvenv.py
> +++ b/python/scripts/mkvenv.py
> @@ -51,6 +51,8 @@
>  import logging
>  import os
>  from pathlib import Path
> +import re
> +import shutil
>  import site
>  import subprocess
>  import sys
> @@ -60,6 +62,7 @@
>      Any,
>      Optional,
>      Sequence,
> +    Tuple,
>      Union,
>  )
>  import venv
> @@ -331,6 +334,128 @@ def _stringify(data: Union[str, bytes]) -> str:
>      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(
> @@ -427,6 +585,15 @@ def _add_ensure_subcommand(subparsers: Any) -> None:
>          action="store",
>          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,
> @@ -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:
> --
> 2.40.1
>
>

ACK, seems good.

(What a lot of work for an error diagnose function that I hope nobody
ever sees, haha!)

--js



  reply	other threads:[~2023-05-16 18:28 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-05-16 10:58 [PATCH v2 01/27] python: shut up "pip install" during "make check-minreqs" Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 02/27] python: update pylint configuration Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 03/27] python: add mkvenv.py Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 04/27] mkvenv: add better error message for broken or missing ensurepip Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 05/27] mkvenv: add nested venv workaround Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 06/27] mkvenv: add ensure subcommand Paolo Bonzini
2023-05-25 20:42   ` John Snow
2023-05-16 10:58 ` [PATCH v2 07/27] mkvenv: add --diagnose option to explain "ensure" failures Paolo Bonzini
2023-05-16 18:27   ` John Snow [this message]
2023-05-16 10:58 ` [PATCH v2 08/27] mkvenv: add console script entry point generation Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 09/27] mkvenv: use pip's vendored distlib as a fallback Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 10/27] mkvenv: avoid ensurepip if pip is installed Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 11/27] mkvenv: work around broken pip installations on Debian 10 Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 12/27] tests/docker: add python3-venv dependency Paolo Bonzini
2023-05-26  9:17   ` Philippe Mathieu-Daudé
2023-05-16 10:58 ` [PATCH v2 13/27] tests/vm: Configure netbsd to use Python 3.10 Paolo Bonzini
2023-05-26  9:15   ` Philippe Mathieu-Daudé
2023-05-16 10:58 ` [PATCH v2 14/27] tests/vm: add py310-expat to NetBSD Paolo Bonzini
2023-05-26  9:15   ` Philippe Mathieu-Daudé
2023-05-16 10:58 ` [PATCH v2 15/27] python: add vendor.py utility Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 16/27] configure: create a python venv unconditionally Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 17/27] python/wheels: add vendored meson package Paolo Bonzini
2023-05-16 10:58 ` [PATCH v2 18/27] configure: use 'mkvenv ensure meson' to bootstrap meson Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 19/27] qemu.git: drop meson git submodule Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 20/27] tests: Use configure-provided pyvenv for tests Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 21/27] configure: move --enable-docs and --disable-docs back to configure Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 22/27] configure: bootstrap sphinx with mkvenv Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 23/27] configure: add --enable-pypi and --disable-pypi Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 24/27] Python: Drop support for Python 3.6 Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 25/27] configure: Add courtesy hint to Python version failure message Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 26/27] mkvenv: mark command as required Paolo Bonzini
2023-05-16 10:59 ` [PATCH v2 27/27] python: bump some of the dependencies Paolo Bonzini

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='CAFn=p-be3oyTDFszUH24dOGw2EqbfRyTPxxvq+v3h=nGZVEMKA@mail.gmail.com' \
    --to=jsnow@redhat.com \
    --cc=berrange@redhat.com \
    --cc=pbonzini@redhat.com \
    --cc=philmd@linaro.org \
    --cc=qemu-devel@nongnu.org \
    /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).