diff --git a/news/4120.fixed.md b/news/4120.fixed.md
new file mode 100644
index 0000000000..cafd6ed7b6
--- /dev/null
+++ b/news/4120.fixed.md
@@ -0,0 +1,3 @@
+(pypi) Fixed wheel selection logic so that pure-Python wheels (`none` ABI tag
+and `any` platform tag) match target platform configurations properly, and
+fixed prerelease Python version parsing in `requirements_files_by_platform`.
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index 38ecabc50e..8b06cceaa8 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -387,6 +387,7 @@ bzl_library(
deps = [
":argparse",
":whl_target_platforms",
+ "//python/private:version",
],
)
diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl
index cafe245eb2..07a0784fc1 100644
--- a/python/private/pypi/extension.bzl
+++ b/python/private/pypi/extension.bzl
@@ -92,7 +92,11 @@ def default_platforms():
"marker": "python_version >= '3.13'" if freethreaded else "",
"name": platform_name,
"os_name": "linux",
- "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [
+ "whl_abi_tags": [
+ "none",
+ "cp{major}{minor}t",
+ ] if freethreaded else [
+ "none",
"abi3",
"cp{major}{minor}",
],
@@ -122,7 +126,11 @@ def default_platforms():
"marker": "python_version >= '3.13'" if freethreaded else "",
"name": platform_name,
"os_name": "osx",
- "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [
+ "whl_abi_tags": [
+ "none",
+ "cp{major}{minor}t",
+ ] if freethreaded else [
+ "none",
"abi3",
"cp{major}{minor}",
],
@@ -148,7 +156,11 @@ def default_platforms():
"marker": "python_version >= '3.13'" if freethreaded else "",
"name": platform_name,
"os_name": "windows",
- "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [
+ "whl_abi_tags": [
+ "none",
+ "cp{major}{minor}t",
+ ] if freethreaded else [
+ "none",
"abi3",
"cp{major}{minor}",
],
@@ -171,7 +183,11 @@ def default_platforms():
"marker": "python_version >= '3.13'" if freethreaded else "python_version >= '3.11'",
"name": platform_name,
"os_name": "windows",
- "whl_abi_tags": ["cp{major}{minor}t"] if freethreaded else [
+ "whl_abi_tags": [
+ "none",
+ "cp{major}{minor}t",
+ ] if freethreaded else [
+ "none",
"abi3",
"cp{major}{minor}",
],
diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl
index 596cfb2cbe..c4e5678551 100644
--- a/python/private/pypi/hub_builder.bzl
+++ b/python/private/pypi/hub_builder.bzl
@@ -459,17 +459,25 @@ def _platforms(module_ctx, *, python_version, config, target_platforms):
if values.marker and not evaluate(values.marker, env = env_):
continue
+ whl_abi_tags = [
+ v.format(
+ major = python_version.release[0],
+ minor = python_version.release[1],
+ )
+ for v in values.whl_abi_tags
+ ]
+ if "none" not in whl_abi_tags:
+ whl_abi_tags = ["none"] + whl_abi_tags
+
+ whl_platform_tags = list(values.whl_platform_tags)
+ if "any" not in whl_platform_tags:
+ whl_platform_tags = ["any"] + whl_platform_tags
+
platforms[key] = struct(
env = env_,
triple = "{}_{}_{}".format(abi, values.os_name, values.arch_name),
- whl_abi_tags = [
- v.format(
- major = python_version.release[0],
- minor = python_version.release[1],
- )
- for v in values.whl_abi_tags
- ],
- whl_platform_tags = values.whl_platform_tags,
+ whl_abi_tags = whl_abi_tags,
+ whl_platform_tags = whl_platform_tags,
)
return platforms
diff --git a/python/private/pypi/requirements_files_by_platform.bzl b/python/private/pypi/requirements_files_by_platform.bzl
index c6f53ae611..bfe1405305 100644
--- a/python/private/pypi/requirements_files_by_platform.bzl
+++ b/python/private/pypi/requirements_files_by_platform.bzl
@@ -14,6 +14,7 @@
"""Get the requirement files by platform."""
+load("//python/private:version.bzl", "version")
load(":argparse.bzl", "argparse")
load(":whl_target_platforms.bzl", "whl_target_platforms")
@@ -62,6 +63,15 @@ def _platform(platform_string, python_version = None):
if not python_version or platform_string.startswith("cp"):
return platform_string
+ py_ver = version.parse(python_version) if "." in python_version else None
+ if py_ver and len(py_ver.release) >= 3:
+ return "cp{}{}.{}_{}".format(
+ py_ver.release[0],
+ py_ver.release[1],
+ py_ver.release[2],
+ platform_string,
+ )
+
major, _, tail = python_version.partition(".")
return "cp{}{}_{}".format(major, tail, platform_string)
@@ -159,8 +169,10 @@ def requirements_files_by_platform(
file: [
platform
for filter_or_platform in specifier.split(",")
- for platform in (_default_platforms(filter = filter_or_platform, platforms = platforms) if filter_or_platform.endswith("*") else [filter_or_platform])
- if _platform(platform, python_version) in default_platforms
+ for platform in _default_platforms(
+ filter = filter_or_platform,
+ platforms = platforms,
+ )
]
for file, specifier in requirements_by_platform.items()
}.items()
diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl
index 97849e882d..9de9c25763 100644
--- a/tests/pypi/extension/extension_tests.bzl
+++ b/tests/pypi/extension/extension_tests.bzl
@@ -580,6 +580,22 @@ def _test_extension_dep_coexists_with_concrete_hub(env):
_tests.append(_test_extension_dep_coexists_with_concrete_hub)
+def _test_default_platforms_whl_abi_tags(env):
+ """Verify that default_platforms includes 'none' in whl_abi_tags."""
+ platforms = default_platforms()
+ for name, plat in platforms.items():
+ env.expect.that_collection(
+ plat["whl_abi_tags"],
+ expr = "{}[whl_abi_tags]".format(name),
+ ).contains("none")
+ if "_freethreaded" not in name:
+ env.expect.that_collection(
+ plat["whl_abi_tags"],
+ expr = "{}[whl_abi_tags]".format(name),
+ ).contains("abi3")
+
+_tests.append(_test_default_platforms_whl_abi_tags)
+
def extension_test_suite(name):
"""Create the test suite.
diff --git a/tests/pypi/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl
index f421c7e351..7a38f19734 100644
--- a/tests/pypi/hub_builder/hub_builder_tests.bzl
+++ b/tests/pypi/hub_builder/hub_builder_tests.bzl
@@ -1608,6 +1608,93 @@ Attempting to create a duplicate library pypi_315_foo for foo with different arg
_tests.append(_test_err_duplicate_repos)
+def _test_platforms_without_none_or_any_tags(env):
+ """Verify hub_builder behavior when tags are missing.
+
+ Ensures 'none' is added to whl_abi_tags and 'any' to whl_platform_tags.
+ """
+
+ def mockread_simpleapi(*_, parse_index, **__):
+ if parse_index:
+ content = """\
+simple-0.0.1-py3-none-any.whl
\
+"""
+ return struct(
+ output = parse_simpleapi_html(
+ content = content,
+ parse_index = parse_index,
+ ),
+ success = True,
+ )
+
+ builder = hub_builder(
+ env,
+ config = struct(
+ netrc = None,
+ enable_pipstar_extract = True,
+ index_url = "https://pypi.org/simple",
+ auth_patterns = {},
+ platforms = {
+ "linux_x86_64": struct(
+ name = "linux_x86_64",
+ os_name = "linux",
+ arch_name = "x86_64",
+ config_settings = [
+ "@platforms//os:linux",
+ "@platforms//cpu:x86_64",
+ ],
+ env = {"implementation_name": "cpython"},
+ marker = "",
+ whl_abi_tags = ["cp{major}{minor}"],
+ whl_platform_tags = ["linux_x86_64", "manylinux_*_x86_64"],
+ ),
+ },
+ ),
+ available_interpreters = {
+ "python_3_15_host": "unit_test_interpreter_target",
+ },
+ minor_mapping = {"3.15": "3.15.19"},
+ simpleapi_download_fn = lambda *args, **kwargs: simpleapi_download(
+ read_simpleapi = mockread_simpleapi,
+ *args,
+ **kwargs
+ ),
+ )
+ builder.pip_parse(
+ mocks.mctx(
+ mock_files = {
+ "requirements.txt": "simple==0.0.1 --hash=sha256:deadbeef",
+ },
+ os_name = "linux",
+ arch_name = "x86_64",
+ ),
+ _parse(
+ hub_name = "pypi",
+ python_version = "3.15",
+ experimental_index_url = "https://example.com",
+ requirements_lock = "requirements.txt",
+ target_platforms = ["linux_x86_64"],
+ ),
+ )
+ pypi = builder.build()
+
+ pypi.exposed_packages().contains_exactly(["simple"])
+ pypi.whl_map().contains_exactly({
+ "simple": {
+ "pypi_315_simple_py3_none_any_deadbeef": [
+ whl_config_setting(
+ target_platforms = ["cp315_linux_x86_64"],
+ version = "3.15",
+ ),
+ ],
+ },
+ })
+
+_tests.append(_test_platforms_without_none_or_any_tags)
+
def hub_builder_test_suite(name):
"""Create the test suite.
diff --git a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl
index 7f4260081b..370dadad3a 100644
--- a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl
+++ b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl
@@ -328,6 +328,53 @@ def _test_uv_lock_only(env):
_tests.append(_test_uv_lock_only)
+def _test_prerelease_python_version(env):
+ """Test that prerelease python_version prefixes platform names.
+
+ For example, 3.13.0rc1 prefixes platform names with cp313.0_.
+ """
+ got = requirements_files_by_platform(
+ requirements_lock = "requirements_lock",
+ python_version = "3.13.0rc1",
+ )
+ env.expect.that_dict(got).contains_exactly({
+ "requirements_lock": [
+ "cp313.0_linux_aarch64",
+ "cp313.0_linux_arm",
+ "cp313.0_linux_ppc",
+ "cp313.0_linux_s390x",
+ "cp313.0_linux_x86_64",
+ "cp313.0_osx_aarch64",
+ "cp313.0_osx_x86_64",
+ "cp313.0_windows_x86_64",
+ ],
+ })
+
+_tests.append(_test_prerelease_python_version)
+
+def _test_requirements_by_platform_exact_platform(env):
+ """Test requirements_by_platform with exact platform names.
+
+ Verifies behavior without trailing wildcard.
+ """
+ got = requirements_files_by_platform(
+ requirements_by_platform = {
+ "req_linux": "linux_x86_64",
+ "req_osx": "osx_x86_64",
+ },
+ platforms = [
+ "cp315.19_linux_x86_64",
+ "cp315.19_osx_x86_64",
+ ],
+ python_version = "3.15.19",
+ )
+ env.expect.that_dict(got).contains_exactly({
+ "req_linux": ["cp315.19_linux_x86_64"],
+ "req_osx": ["cp315.19_osx_x86_64"],
+ })
+
+_tests.append(_test_requirements_by_platform_exact_platform)
+
def _test_uv_lock_with_platform_arg(env):
"""Verify that using uv_lock with --platform argument succeeds."""
got = requirements_files_by_platform(