Fix #130561: incorrect Python compatibility checks for wheels
Regression in [0] caused wheels with an older Python version
that used CPython's stable ABI to be considered incompatible.
Resolve using the stable ABI for version checks.
[0]: cfc10b0232
This commit is contained in:
committed by
Thomas Dinges
parent
ead4f5a898
commit
cf8a569905
@@ -950,6 +950,7 @@ def _extensions_wheel_filter_for_this_system(wheels):
|
||||
import sys
|
||||
from .bl_extension_utils import (
|
||||
python_versions_from_wheel_python_tag,
|
||||
python_versions_from_wheel_abi_tag,
|
||||
)
|
||||
|
||||
python_version_current = sys.version_info[:2]
|
||||
@@ -974,8 +975,8 @@ def _extensions_wheel_filter_for_this_system(wheels):
|
||||
if not (5 <= len(wheel_filename_split) <= 6):
|
||||
print("Error: wheel doesn't follow naming spec \"{:s}\"".format(wheel_filename))
|
||||
continue
|
||||
# TODO: Match ABI tags.
|
||||
python_tag, _abi_tag, platform_tag = wheel_filename_split[-3:]
|
||||
|
||||
python_tag, abi_tag, platform_tag = wheel_filename_split[-3:]
|
||||
|
||||
# Perform Platform Checks.
|
||||
if platform_tag in {"any", platform_tag_current}:
|
||||
@@ -1020,6 +1021,22 @@ def _extensions_wheel_filter_for_this_system(wheels):
|
||||
if python_version_current == python_version:
|
||||
python_version_is_compat = True
|
||||
break
|
||||
|
||||
# When there is a stable ABI: Allow an older Python wheel to be compatible
|
||||
# with a newer Python as long as the older wheel uses the stable ABI, see:
|
||||
# https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#abi-tag
|
||||
if isinstance(
|
||||
python_versions_stable_abi := python_versions_from_wheel_abi_tag(abi_tag, stable_only=True),
|
||||
str,
|
||||
):
|
||||
print("Error: wheel \"{:s}\" unable to parse Python ABI version {:s}".format(
|
||||
wheel_filename, python_versions,
|
||||
))
|
||||
elif (python_version_current[0],) in python_versions_stable_abi:
|
||||
if python_version_current >= python_version:
|
||||
python_version_is_compat = True
|
||||
break
|
||||
|
||||
if not python_version_is_compat:
|
||||
# Useful to know, can quiet print in the future.
|
||||
print(
|
||||
|
||||
@@ -838,6 +838,13 @@ def python_versions_from_wheel_python_tag(python_tag: str) -> set[tuple[int] | t
|
||||
return result
|
||||
|
||||
|
||||
def python_versions_from_wheel_abi_tag(abi_tag: str, *, stable_only: bool) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
from .cli.blender_ext import python_versions_from_wheel_abi_tag as fn
|
||||
result = fn(abi_tag, stable_only=stable_only)
|
||||
assert isinstance(result, (set, str))
|
||||
return result
|
||||
|
||||
|
||||
def python_versions_from_wheels(wheel_files: Sequence[str]) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
from .cli.blender_ext import python_versions_from_wheels as fn
|
||||
result = fn(wheel_files)
|
||||
|
||||
@@ -2209,11 +2209,9 @@ def python_versions_from_wheel_python_tag(python_tag: str) -> set[tuple[int] | t
|
||||
"""
|
||||
Return Python versions from a wheels ``python_tag``.
|
||||
"""
|
||||
# Index backwards to skip the optional build tag.
|
||||
# The version may be:
|
||||
# `cp312` for CPython 3.12
|
||||
# `py2.py3` for both Python 2 & 3.
|
||||
versions_string = python_tag.split(".")
|
||||
|
||||
# Based on the documentation as of 2024 and wheels used by existing extensions,
|
||||
# these are the only valid prefix values.
|
||||
@@ -2221,12 +2219,10 @@ def python_versions_from_wheel_python_tag(python_tag: str) -> set[tuple[int] | t
|
||||
|
||||
versions: set[tuple[int] | tuple[int, int]] = set()
|
||||
|
||||
for version_string in versions_string:
|
||||
m = RE_PYTHON_WHEEL_VERSION_TAG.match(version_string)
|
||||
for tag in python_tag.split("."):
|
||||
m = RE_PYTHON_WHEEL_VERSION_TAG.match(tag)
|
||||
if m is None:
|
||||
return "wheel filename version could not be extracted from: \"{:s}\"".format(
|
||||
version_string,
|
||||
)
|
||||
return "wheel filename version could not be extracted from: \"{:s}\"".format(tag)
|
||||
|
||||
version_prefix = m.group(1).lower()
|
||||
version_number = m.group(2)
|
||||
@@ -2252,6 +2248,25 @@ def python_versions_from_wheel_python_tag(python_tag: str) -> set[tuple[int] | t
|
||||
return versions
|
||||
|
||||
|
||||
def python_versions_from_wheel_abi_tag(
|
||||
abi_tag: str,
|
||||
*,
|
||||
stable_only: bool,
|
||||
) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
versions: set[tuple[int] | tuple[int, int]] = set()
|
||||
|
||||
# Not yet needed, add as needed.
|
||||
if stable_only:
|
||||
for tag in abi_tag.split("."):
|
||||
if tag.startswith("abi") and tag[3:].isdigit():
|
||||
versions.add((int(tag[3:]),))
|
||||
else:
|
||||
# Not a problem to support this, currently it's not needed.
|
||||
raise NotImplementedError
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def python_versions_from_wheel(wheel_filename: str) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
"""
|
||||
Extract a set of Python versions from a list of wheels or return an error string.
|
||||
@@ -2262,7 +2277,25 @@ def python_versions_from_wheel(wheel_filename: str) -> set[tuple[int] | tuple[in
|
||||
if not (5 <= len(wheel_filename_split) <= 6):
|
||||
return "wheel filename must follow the spec \"{:s}\", found {!r}".format(WHEEL_FILENAME_SPEC, wheel_filename)
|
||||
|
||||
return python_versions_from_wheel_python_tag(wheel_filename_split[-3])
|
||||
python_tag = wheel_filename_split[-3]
|
||||
abi_tag = wheel_filename_split[-2]
|
||||
|
||||
# NOTE(@ideasman42): when the ABI is set, simply return the major version,
|
||||
# This is needed because older version of CPython (3.6) for e.g. are compatible with newer versions of CPython,
|
||||
# but returning the old version causes it not to register as being compatible.
|
||||
# So return the ABI version to allow any version of CPython 3.x.
|
||||
#
|
||||
# There is a logical problem here: which is that a future wheel from CPython *should* be detected
|
||||
# as incompatible but wont be. To properly support this, extensions repository data would need to store
|
||||
# either store a separate ABI version or a `>=` version. In practice this isn't as bad as it sounds
|
||||
# because those packages typically won't support old versions of Blender known to use older Python versions,
|
||||
# although it will incorrectly exclude old versions of Blender which were built against newer versions of
|
||||
# CPython than the version used by official builds.
|
||||
python_versions_from_abi = python_versions_from_wheel_abi_tag(abi_tag, stable_only=True)
|
||||
if python_versions_from_abi:
|
||||
return python_versions_from_abi
|
||||
|
||||
return python_versions_from_wheel_python_tag(python_tag)
|
||||
|
||||
|
||||
def python_versions_from_wheels(wheel_files: Sequence[str]) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
|
||||
@@ -218,7 +218,7 @@ def create_package(
|
||||
fh.write('''version = "1.0.0"\n''')
|
||||
fh.write('''tagline = "This is a tagline"\n''')
|
||||
fh.write('''blender_version_min = "{:s}"\n'''.format(blender_version_min or "0.0.0"))
|
||||
if blender_version_min is not None:
|
||||
if blender_version_max is not None:
|
||||
fh.write('''blender_version_max = "{:s}"\n'''.format(blender_version_max))
|
||||
fh.write('''\n''')
|
||||
|
||||
@@ -915,6 +915,97 @@ class TestPythonVersions(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
self.assertEqual(stderr, "")
|
||||
self.assertEqual(stdout, '''found 3 packages.\n''')
|
||||
|
||||
def test_server_generate_version_with_stable_abi(self) -> None:
|
||||
# The different Python versions in the wheels cause the packages not to conflict.
|
||||
repo_id = "test_repo_stable_abi_versions"
|
||||
repo_name = "MyTestRepoVersionStableABI"
|
||||
|
||||
self.repo_add(repo_id=repo_id, repo_name=repo_name)
|
||||
|
||||
this_python_version_major, this_python_version_minor = sys.version_info[:2]
|
||||
# TODO: this test doesn't make sense for the first Python major releases (4.0 for e.g.).
|
||||
if this_python_version_minor == 0:
|
||||
return
|
||||
|
||||
python_abi_current_stable = "abi{:d}".format(this_python_version_major)
|
||||
python_abi_current = "cp{:d}{:d}".format(this_python_version_major, this_python_version_minor)
|
||||
python_abi_outdated = "cp{:d}{:d}".format(this_python_version_major, this_python_version_minor - 1)
|
||||
|
||||
pkg_idnames = (
|
||||
# Python incompatible because it's stable ABI implies gerater-or-equal-to.
|
||||
("my_test_pkg_a", "my_test_a", [
|
||||
"example_A-1.2.3-{0:s}-{0:s}-any.whl".format(
|
||||
# Old ABI (making this extension incompatible).
|
||||
python_abi_outdated,
|
||||
),
|
||||
"example_B-1.2.3-{0:s}-{0:s}-any.whl".format(python_abi_outdated),
|
||||
]),
|
||||
# Python compatible.
|
||||
("my_test_pkg_b", "my_test_b", [
|
||||
"example_A-1.2.3-{:s}-{:s}-any.whl".format(
|
||||
python_abi_outdated,
|
||||
# Stable ABI (making this extension compatible).
|
||||
python_abi_current_stable,
|
||||
),
|
||||
"example_B-1.2.3-{0:s}-{0:s}-any.whl".format(python_abi_current),
|
||||
]),
|
||||
)
|
||||
|
||||
# Create a package contents.
|
||||
for pkg_idname, pkg_filename, wheel_filenames in pkg_idnames:
|
||||
wheel_params = []
|
||||
for wheel_filename in wheel_filenames:
|
||||
module_name, module_version = wheel_filename.split("-", 2)[0:2]
|
||||
wheel_params.append(
|
||||
WheelModuleParams(
|
||||
module_name=module_name,
|
||||
module_version=module_version,
|
||||
filename=wheel_filename,
|
||||
),
|
||||
)
|
||||
del module_name, module_version
|
||||
|
||||
self.build_package(
|
||||
pkg_idname=pkg_idname,
|
||||
pkg_filename=pkg_filename,
|
||||
blender_version_min="4.2.0",
|
||||
wheel_params=wheel_params,
|
||||
)
|
||||
|
||||
# Generate the repository.
|
||||
returncode, stdout, stderr = run_blender_extensions((
|
||||
"server-generate",
|
||||
"--repo-dir", TEMP_DIR_REMOTE,
|
||||
))
|
||||
self.assertEqual(returncode, 0)
|
||||
self.assertEqual(stderr, "")
|
||||
self.assertEqual(stdout, '''found 2 packages.\n''')
|
||||
|
||||
stdout = run_blender_extensions_no_errors((
|
||||
"sync",
|
||||
))
|
||||
self.assertEqual(
|
||||
stdout.rstrip("\n").split("\n")[-1],
|
||||
"STATUS Extensions list for \"{:s}\" updated".format(repo_name),
|
||||
)
|
||||
|
||||
# This should be filtered out.
|
||||
# NOTE: ideally an error would show an error that the extension is known but not compatible.
|
||||
returncode, stdout, stderr = run_blender_extensions(("install", "my_test_pkg_a", "--enable"))
|
||||
|
||||
self.assertEqual(stdout.rstrip(), "")
|
||||
self.assertEqual(returncode, 1)
|
||||
self.assertEqual(
|
||||
"Package \"my_test_pkg_a\" not found in remote repositories!",
|
||||
stderr.rstrip(),
|
||||
)
|
||||
|
||||
stdout = run_blender_extensions_no_errors(("install", "my_test_pkg_b", "--enable"))
|
||||
self.assertEqual(
|
||||
[line for line in stdout.split("\n") if line.startswith("STATUS ")][0],
|
||||
"STATUS Installed \"my_test_pkg_b\""
|
||||
)
|
||||
|
||||
|
||||
class TestModuleViolation(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user