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(