Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions news/4120.fixed.md
Original file line number Diff line number Diff line change
@@ -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`.
1 change: 1 addition & 0 deletions python/private/pypi/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ bzl_library(
deps = [
":argparse",
":whl_target_platforms",
"//python/private:version",
],
)

Expand Down
24 changes: 20 additions & 4 deletions python/private/pypi/extension.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
],
Expand Down Expand Up @@ -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}",
],
Expand All @@ -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}",
],
Expand All @@ -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}",
],
Expand Down
24 changes: 16 additions & 8 deletions python/private/pypi/hub_builder.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions python/private/pypi/requirements_files_by_platform.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 16 additions & 0 deletions tests/pypi/extension/extension_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
87 changes: 87 additions & 0 deletions tests/pypi/hub_builder/hub_builder_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """<a href="/simple/>simple</a><br/>"""
else:
content = """\
<a href="/simple-0.0.1-py3-none-any.whl#sha256=deadbeef">\
simple-0.0.1-py3-none-any.whl</a><br/>\
"""
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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down