On 11.08.2025 22:01, John Snow wrote:
From: "Sv. Lockal" <lockals...@gmail.com>

Fix compilation with pip-25.2 due to missing distlib.version

Bug: https://gitlab.com/qemu-project/qemu/-/issues/3062

Signed-off-by: Sv. Lockal <lockals...@gmail.com>
[Edits: Type "safety" whackamole --js]
Signed-off-by: John Snow <js...@redhat.com>
---
  python/scripts/mkvenv.py | 64 +++++++++++++++++++++++++++++++++++++---
  1 file changed, 60 insertions(+), 4 deletions(-)

I think with this in place, there's no need to do any fallbacks here, --
replace distlib.version.LegacyMatcher with packaging.requirements.Requirement
directly in the code, and import it from pip directly without any "try"
and fallbacks.

Oldest pip we support (21.3 iirc) has "packaging" available.  There's
just no need for all this hackery/complexity.

When I first saw the patch I though it's the way to go.  But now when
I thought about it, I think it's better to just throw away all this
complexity.

It's sort of risky because we didn't verify how it works on all
supported systems, but it "should" be the same :)

/mjt

diff --git a/python/scripts/mkvenv.py b/python/scripts/mkvenv.py
index 8ac5b0b2a05..f102527c4de 100644
--- a/python/scripts/mkvenv.py
+++ b/python/scripts/mkvenv.py
@@ -84,6 +84,7 @@
      Sequence,
      Tuple,
      Union,
+    cast,
  )
  import venv
@@ -94,17 +95,39 @@
  HAVE_DISTLIB = True
  try:
      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.scripts  # noqa, pylint: 
disable=unused-import
-        import pip._vendor.distlib.version  # noqa, pylint: 
disable=unused-import
      except ImportError:
          HAVE_DISTLIB = False
+# pip 25.2 does not vendor distlib.version, but it uses vendored
+# packaging.version
+HAVE_DISTLIB_VERSION = True
+try:
+    import distlib.version  # pylint: disable=ungrouped-imports
+except ImportError:
+    try:
+        # pylint: disable=unused-import,ungrouped-imports
+        import pip._vendor.distlib.version  # noqa
+    except ImportError:
+        HAVE_DISTLIB_VERSION = False
+
+HAVE_PACKAGING_VERSION = True
+try:
+    # Do not bother importing non-vendored packaging, because it is not
+    # in stdlib.
+    from pip._vendor import packaging
+    # pylint: disable=unused-import
+    import pip._vendor.packaging.requirements  # noqa
+    import pip._vendor.packaging.version  # noqa
+except ImportError:
+    HAVE_PACKAGING_VERSION = False
+
+
  # Try to load tomllib, with a fallback to tomli.
  # HAVE_TOMLLIB is checked below, just-in-time, so that mkvenv does not fail
  # outside the venv or before a potential call to ensurepip in checkpip().
@@ -133,6 +156,39 @@ class Ouch(RuntimeError):
      """An Exception class we can't confuse with a builtin."""
+class Matcher:
+    """Compatibility appliance for version/requirement string parsing."""
+    def __init__(self, name_and_constraint: str):
+        """Create a matcher from a requirement-like string."""
+        if HAVE_DISTLIB_VERSION:
+            self._m = distlib.version.LegacyMatcher(name_and_constraint)
+        elif HAVE_PACKAGING_VERSION:
+            self._m = packaging.requirements.Requirement(name_and_constraint)
+        else:
+            raise Ouch("found neither distlib.version nor packaging.version")
+        self.name = self._m.name
+
+    def match(self, version_str: str) -> bool:
+        """Return True if `version` satisfies the stored constraint."""
+        if HAVE_DISTLIB_VERSION:
+            return cast(
+                bool,
+                self._m.match(distlib.version.LegacyVersion(version_str))
+            )
+
+        assert HAVE_PACKAGING_VERSION
+        return cast(
+            bool,
+            self._m.specifier.contains(
+                packaging.version.Version(version_str), prereleases=True
+            )
+        )
+
+    def __repr__(self) -> str:
+        """Stable debug representation delegated to the backend."""
+        return repr(self._m)
+
+
  class QemuEnvBuilder(venv.EnvBuilder):
      """
      An extension of venv.EnvBuilder for building QEMU's configure-time venv.
@@ -669,7 +725,7 @@ def _do_ensure(
      canary = None
      for name, info in group.items():
          constraint = _make_version_constraint(info, False)
-        matcher = distlib.version.LegacyMatcher(name + constraint)
+        matcher = Matcher(name + constraint)
          print(f"mkvenv: checking for {matcher}", file=sys.stderr)
dist: Optional[Distribution] = None
@@ -683,7 +739,7 @@ def _do_ensure(
              # Always pass installed package to pip, so that they can be
              # updated if the requested version changes
              or not _is_system_package(dist)
-            or not matcher.match(distlib.version.LegacyVersion(dist.version))
+            or not matcher.match(dist.version)
          ):
              absent.append(name + _make_version_constraint(info, True))
              if len(absent) == 1:


Reply via email to