From 07da277e89290af9fe80a3273dea76eaef9a38bf Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 28 Aug 2026 10:46:01 -0700 Subject: [PATCH 1/4] tests: add test cases for none and any wheel tags and prerelease platform parsing Add tests reproducing wheel resolution tag handling in default_platforms and hub_builder, along with prerelease Python version platform prefixing and exact platform matching in requirements_files_by_platform. --- tests/pypi/extension/extension_tests.bzl | 16 ++++ tests/pypi/hub_builder/hub_builder_tests.bzl | 84 +++++++++++++++++++ .../requirements_files_by_platform_tests.bzl | 41 +++++++++ 3 files changed, 141 insertions(+) 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..63382223d9 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -1608,6 +1608,90 @@ 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 adds 'none' in whl_abi_tags and 'any' in 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..f6763c2e04 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,47 @@ def _test_uv_lock_only(env): _tests.append(_test_uv_lock_only) +def _test_prerelease_python_version(env): + """Test that prerelease python_version (e.g. 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 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( From 61818c32c4112f0bf72d00ef5a6c309644379ec3 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 28 Aug 2026 10:48:13 -0700 Subject: [PATCH 2/4] fix(pypi): include none and any tags in wheel selection and fix prerelease platform parsing Ensure pure-Python wheels can match target platforms by including 'none' in default ABI tags and guaranteeing 'none' and 'any' tags in hub builder platform descriptors. Fix prerelease Python version string parsing in requirements_files_by_platform to properly format platform keys. --- news/whl_abi_tags.fixed.md | 3 +++ python/private/pypi/BUILD.bazel | 1 + python/private/pypi/extension.bzl | 24 +++++++++++++++---- python/private/pypi/hub_builder.bzl | 24 ++++++++++++------- .../pypi/requirements_files_by_platform.bzl | 8 +++++-- 5 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 news/whl_abi_tags.fixed.md diff --git a/news/whl_abi_tags.fixed.md b/news/whl_abi_tags.fixed.md new file mode 100644 index 0000000000..cafd6ed7b6 --- /dev/null +++ b/news/whl_abi_tags.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..3913d8cddb 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,10 @@ 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 +164,7 @@ 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() From 9623f89a2cf352d1a2347c24cbd86df7f64c3776 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 28 Aug 2026 10:50:49 -0700 Subject: [PATCH 3/4] refactor: wrap long lines in starlark files and tests Wrap docstrings and expressions to conform to the 80 column line limit across pypi Starlark files and analysis tests. --- .../private/pypi/requirements_files_by_platform.bzl | 12 ++++++++++-- tests/pypi/hub_builder/hub_builder_tests.bzl | 5 ++++- .../requirements_files_by_platform_tests.bzl | 10 ++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/python/private/pypi/requirements_files_by_platform.bzl b/python/private/pypi/requirements_files_by_platform.bzl index 3913d8cddb..bfe1405305 100644 --- a/python/private/pypi/requirements_files_by_platform.bzl +++ b/python/private/pypi/requirements_files_by_platform.bzl @@ -65,7 +65,12 @@ def _platform(platform_string, python_version = None): 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) + return "cp{}{}.{}_{}".format( + py_ver.release[0], + py_ver.release[1], + py_ver.release[2], + platform_string, + ) major, _, tail = python_version.partition(".") @@ -164,7 +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) + 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/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl index 63382223d9..7a38f19734 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -1609,7 +1609,10 @@ 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 adds 'none' in whl_abi_tags and 'any' in whl_platform_tags.""" + """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: 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 f6763c2e04..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 @@ -329,7 +329,10 @@ def _test_uv_lock_only(env): _tests.append(_test_uv_lock_only) def _test_prerelease_python_version(env): - """Test that prerelease python_version (e.g. 3.13.0rc1) prefixes platform names with cp313.0_.""" + """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", @@ -350,7 +353,10 @@ def _test_prerelease_python_version(env): _tests.append(_test_prerelease_python_version) def _test_requirements_by_platform_exact_platform(env): - """Test requirements_by_platform with exact platform names without trailing wildcard.""" + """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", From bcfcc08cee578cbb9290ef8e324b31e4faeaf689 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 28 Aug 2026 10:51:44 -0700 Subject: [PATCH 4/4] docs: rename news entry to match PR number 4120 Rename news/whl_abi_tags.fixed.md to news/4120.fixed.md to align with the created draft PR number. --- news/{whl_abi_tags.fixed.md => 4120.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{whl_abi_tags.fixed.md => 4120.fixed.md} (100%) diff --git a/news/whl_abi_tags.fixed.md b/news/4120.fixed.md similarity index 100% rename from news/whl_abi_tags.fixed.md rename to news/4120.fixed.md