Skip to content

Commit 5fa1a87

Browse files
aignasrickeylevgemini-code-assist[bot]
authored
fix(pypi): select the lowest available libc version by default (#3255)
The #3058 PR has subtly changed the default behaviour of `experimental_index_url` code path and I think in order to make things easier by default for our users we should go back to that behaviour. And in addition to this we are starting to make use of the Minimal Version Selection algorithm for the platforms. This in general allows users to configure the upper platform version for a particular wheel. This meant that we had to change the semantics of the API a little: 1. Use MVS for each platform platform tag. 2. Make it such that earlier entries are overridden by later ones, i.e. `["musllinux_*_x86_64", "musllinux_1_2_x86_64"]` is effectively the same as just `["musllinux_1_2_x86_64"]`. A remaining thing that will be left as a followup for #2747 will be to figure out how to allow users to ignore certain platform tags. Fixes #3250 --------- Co-authored-by: Richard Levasseur <[email protected]> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 668a551 commit 5fa1a87

File tree

4 files changed

+199
-35
lines changed

4 files changed

+199
-35
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ END_UNRELEASED_TEMPLATE
7979
length errors due to too long environment variables.
8080
* (bootstrap) {obj}`--bootstrap_impl=script` now supports the `-S` interpreter
8181
setting.
82+
* (pypi) We now use the Minimal Version Selection (MVS) algorithm to select
83+
the right wheel when there are multiple wheels for the target platform
84+
(e.g. `musllinux_1_1_x86_64` and `musllinux_1_2_x86_64`). If the user
85+
wants to set the minimum version for the selection algorithm, use the
86+
{attr}`pip.defaults.whl_platform_tags` attribute to configure that. If
87+
`musllinux_*_x86_64` is specified, we will chose the lowest available
88+
wheel version. Fixes [#3250](https://github.com/bazel-contrib/rules_python/issues/3250).
8289

8390
{#v0-0-0-added}
8491
### Added

python/private/pypi/extension.bzl

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -492,35 +492,37 @@ preference.
492492
Will always include `"any"` even if it is not specified.
493493
494494
The items in this list can contain a single `*` character that is equivalent to matching the
495-
latest available version component in the platform_tag. Note, if the wheel platform tag does not
496-
have a version component, e.g. `linux_x86_64` or `win_amd64`, then `*` will act as a regular
497-
character.
498-
499-
We will always select the highest available `platform_tag` version that is compatible with the
500-
target platform.
495+
lowest available version component in the platform_tag. If the wheel platform tag does not
496+
have a version component, e.g. `linux_x86_64` or `win_amd64`, then `*` will act as a regular character.
501497
502498
:::{note}
499+
Normally, the `*` in the matcher means that we will target the lowest platform version that we can
500+
and will give preference to whls built targeting the older versions of the platform. If you
501+
specify the version, then we will use the MVS (Minimal Version Selection) algorithm to select the
502+
compatible wheel. As such, you need to keep in mind how to configure the target platforms to
503+
select a particular wheel of your preference.
504+
503505
We select a single wheel and the last match will take precedence, if the platform_tag that we
504506
match has a version component (e.g. `android_x_arch`, then the version `x` will be used in the
505-
matching algorithm).
506-
507-
If the matcher you provide has `*`, then we will match a wheel with the highest available target platform, i.e. if `musllinux_1_1_arch` and `musllinux_1_2_arch` are both present, then we will select `musllinux_1_2_arch`.
508-
Otherwise we will select the highest available version that is equal or lower to the specifier, i.e. if `manylinux_2_12` and `manylinux_2_17` wheels are present and the matcher is `manylinux_2_15`, then we will match `manylinux_2_12` but not `manylinux_2_17`.
509-
:::
510-
511-
:::{note}
512-
The following tag prefixes should be used instead of the legacy equivalents:
513-
* `manylinux_2_5` instead of `manylinux1`
514-
* `manylinux_2_12` instead of `manylinux2010`
515-
* `manylinux_2_17` instead of `manylinux2014`
516-
517-
When parsing the whl filenames `rules_python` will automatically transform wheel filenames to the
518-
latest format.
507+
MVS matching algorithm).
508+
509+
Common patterns:
510+
* To select any versioned wheel for an `<os>`, `<arch>`, use `<os>_*_<arch>`, e.g.
511+
`manylinux_2_17_x86_64`.
512+
* To exclude versions up to `X.Y` - **submit a PR supporting this feature**.
513+
* To exclude versions above `X.Y`, provide the full platform tag specifier, e.g.
514+
`musllinux_1_2_x86_64`, which will ensure that no wheels with `musllinux_1_3_x86_64` or higher
515+
are selected.
519516
:::
520517
521518
:::{seealso}
522519
See official [docs](https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#platform-tag) for more information.
523520
:::
521+
:::{versionchanged} VERSION_NEXT_FEATURE
522+
The matching of versioned platforms have been switched to MVS (Minimal Version Selection)
523+
algorithm for easier evaluation logic and fewer surprises. The legacy platform tags are
524+
supported from this version without extra handling from the user.
525+
:::
524526
""",
525527
),
526528
} | AUTH_ATTRS

python/private/pypi/select_whl.bzl

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ _MANYLINUX = "manylinux"
1010
_MACOSX = "macosx"
1111
_MUSLLINUX = "musllinux"
1212

13+
# Taken from https://peps.python.org/pep-0600/
14+
_LEGACY_ALIASES = {
15+
"manylinux1_i686": "manylinux_2_5_i686",
16+
"manylinux1_x86_64": "manylinux_2_5_x86_64",
17+
"manylinux2010_i686": "manylinux_2_12_i686",
18+
"manylinux2010_x86_64": "manylinux_2_12_x86_64",
19+
"manylinux2014_aarch64": "manylinux_2_17_aarch64",
20+
"manylinux2014_armv7l": "manylinux_2_17_armv7l",
21+
"manylinux2014_i686": "manylinux_2_17_i686",
22+
"manylinux2014_ppc64": "manylinux_2_17_ppc64",
23+
"manylinux2014_ppc64le": "manylinux_2_17_ppc64le",
24+
"manylinux2014_s390x": "manylinux_2_17_s390x",
25+
"manylinux2014_x86_64": "manylinux_2_17_x86_64",
26+
}
27+
1328
def _value_priority(*, tag, values):
1429
keys = []
1530
for priority, wp in enumerate(values):
@@ -18,17 +33,61 @@ def _value_priority(*, tag, values):
1833

1934
return max(keys) if keys else None
2035

21-
def _platform_tag_priority(*, tag, values):
22-
# Implements matching platform tag
23-
# https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/
24-
25-
if not (
36+
def _is_platform_tag_versioned(tag):
37+
return (
2638
tag.startswith(_ANDROID) or
2739
tag.startswith(_IOS) or
2840
tag.startswith(_MACOSX) or
2941
tag.startswith(_MANYLINUX) or
3042
tag.startswith(_MUSLLINUX)
31-
):
43+
)
44+
45+
def _parse_platform_tags(tags):
46+
"""A helper function that parses all of the platform tags.
47+
48+
The main idea is to make this more robust and have better debug messages about which will
49+
is compatible and which is not with the target platform.
50+
"""
51+
ret = []
52+
replacements = {}
53+
for tag in tags:
54+
tag = _LEGACY_ALIASES.get(tag, tag)
55+
56+
if not _is_platform_tag_versioned(tag):
57+
ret.append(tag)
58+
continue
59+
60+
want_os, sep, tail = tag.partition("_")
61+
if not sep:
62+
fail("could not parse the tag: {}".format(tag))
63+
64+
want_major, _, tail = tail.partition("_")
65+
if want_major == "*":
66+
# the expected match is any version
67+
want_arch = tail
68+
elif want_os.startswith(_ANDROID):
69+
want_arch = tail
70+
else:
71+
# drop the minor version segment
72+
_, _, want_arch = tail.partition("_")
73+
74+
placeholder = "{}_*_{}".format(want_os, want_arch)
75+
replacements[placeholder] = tag
76+
if placeholder in ret:
77+
ret.remove(placeholder)
78+
79+
ret.append(placeholder)
80+
81+
return [
82+
replacements.get(p, p)
83+
for p in ret
84+
]
85+
86+
def _platform_tag_priority(*, tag, values):
87+
# Implements matching platform tag
88+
# https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/
89+
90+
if not _is_platform_tag_versioned(tag):
3291
res = _value_priority(tag = tag, values = values)
3392
if res == None:
3493
return res
@@ -39,7 +98,7 @@ def _platform_tag_priority(*, tag, values):
3998

4099
os, _, tail = tag.partition("_")
41100
major, _, tail = tail.partition("_")
42-
if not os.startswith(_ANDROID):
101+
if not tag.startswith(_ANDROID):
43102
minor, _, arch = tail.partition("_")
44103
else:
45104
minor = "0"
@@ -65,7 +124,7 @@ def _platform_tag_priority(*, tag, values):
65124
want_major = ""
66125
want_minor = ""
67126
want_arch = tail
68-
elif os.startswith(_ANDROID):
127+
elif tag.startswith(_ANDROID):
69128
# we set it to `0` above, so setting the `want_minor` her to `0` will make things
70129
# consistent.
71130
want_minor = "0"
@@ -81,7 +140,7 @@ def _platform_tag_priority(*, tag, values):
81140
# if want_major is defined, then we know that we don't have a `*` in the matcher.
82141
want_version = (int(want_major), int(want_minor)) if want_major else None
83142
if not want_version or version <= want_version:
84-
keys.append((priority, version))
143+
keys.append((priority, (-version[0], -version[1])))
85144

86145
return max(keys) if keys else None
87146

@@ -222,7 +281,7 @@ def select_whl(
222281
implementation_name = implementation_name,
223282
python_version = python_version,
224283
whl_abi_tags = whl_abi_tags,
225-
whl_platform_tags = whl_platform_tags,
284+
whl_platform_tags = _parse_platform_tags(whl_platform_tags),
226285
logger = logger,
227286
)
228287

tests/pypi/select_whl/select_whl_tests.bzl

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ def _test_not_select_abi3(env):
131131
whl_abi_tags = ["none"],
132132
python_version = "3.13",
133133
limit = 2,
134-
debug = True,
135134
)
136135
_match(
137136
env,
@@ -232,6 +231,34 @@ def _test_select_by_supported_cp_version(env):
232231

233232
_tests.append(_test_select_by_supported_cp_version)
234233

234+
def _test_legacy_manylinux(env):
235+
for legacy, replacement in {
236+
"manylinux1": "manylinux_2_5",
237+
"manylinux2010": "manylinux_2_12",
238+
"manylinux2014": "manylinux_2_17",
239+
}.items():
240+
for plat in [legacy, replacement]:
241+
whls = [
242+
"pkg-0.0.1-py3-none-{}_x86_64.whl".format(plat),
243+
"pkg-0.0.1-py3-none-any.whl",
244+
]
245+
246+
got = _select_whl(
247+
whls = whls,
248+
whl_platform_tags = ["{}_x86_64".format(legacy)],
249+
whl_abi_tags = ["none"],
250+
python_version = "3.10",
251+
)
252+
want = _select_whl(
253+
whls = whls,
254+
whl_platform_tags = ["{}_x86_64".format(replacement)],
255+
whl_abi_tags = ["none"],
256+
python_version = "3.10",
257+
)
258+
_match(env, [got], want.filename)
259+
260+
_tests.append(_test_legacy_manylinux)
261+
235262
def _test_supported_cp_version_manylinux(env):
236263
whls = [
237264
"pkg-0.0.1-py2.py3-none-manylinux_1_1_x86_64.whl",
@@ -406,9 +433,10 @@ def _test_multiple_musllinux(env):
406433
_match(
407434
env,
408435
got,
409-
# select the one with the highest version that is matching
410-
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
436+
# select the one with the lowest version that is matching because we want to
437+
# increase the compatibility
411438
"pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
439+
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
412440
)
413441

414442
_tests.append(_test_multiple_musllinux)
@@ -423,17 +451,85 @@ def _test_multiple_musllinux_exact_params(env):
423451
whl_abi_tags = ["none"],
424452
python_version = "3.12",
425453
limit = 2,
454+
debug = True,
426455
)
427456
_match(
428457
env,
429458
got,
430-
# select the one with the lowest version, because of the input to the function
431-
"pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
459+
# 1.2 is not within the candidates because it is not compatible
432460
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
433461
)
434462

435463
_tests.append(_test_multiple_musllinux_exact_params)
436464

465+
def _test_multiple_mvs_match(env):
466+
got = _select_whl(
467+
whls = [
468+
"pkg-0.0.1-py3-none-musllinux_1_4_x86_64.whl",
469+
"pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
470+
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
471+
],
472+
whl_platform_tags = ["musllinux_1_3_x86_64"],
473+
whl_abi_tags = ["none"],
474+
python_version = "3.12",
475+
limit = 2,
476+
)
477+
_match(
478+
env,
479+
got,
480+
# select the one with the lowest version
481+
"pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
482+
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
483+
)
484+
485+
_tests.append(_test_multiple_mvs_match)
486+
487+
def _test_multiple_mvs_match_override_more_specific(env):
488+
got = _select_whl(
489+
whls = [
490+
"pkg-0.0.1-py3-none-musllinux_1_4_x86_64.whl",
491+
"pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
492+
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
493+
],
494+
whl_platform_tags = [
495+
"musllinux_*_x86_64", # default to something
496+
"musllinux_1_3_x86_64", # override the previous
497+
],
498+
whl_abi_tags = ["none"],
499+
python_version = "3.12",
500+
limit = 2,
501+
)
502+
_match(
503+
env,
504+
got,
505+
# Should be the same as without the `*` match
506+
"pkg-0.0.1-py3-none-musllinux_1_2_x86_64.whl",
507+
"pkg-0.0.1-py3-none-musllinux_1_1_x86_64.whl",
508+
)
509+
510+
_tests.append(_test_multiple_mvs_match_override_more_specific)
511+
512+
def _test_multiple_mvs_match_override_less_specific(env):
513+
got = _select_whl(
514+
whls = [
515+
"pkg-0.0.1-py3-none-musllinux_1_4_x86_64.whl",
516+
],
517+
whl_platform_tags = [
518+
"musllinux_1_3_x86_64", # default to 1.3
519+
"musllinux_*_x86_64", # then override to something less specific
520+
],
521+
whl_abi_tags = ["none"],
522+
python_version = "3.12",
523+
limit = 2,
524+
)
525+
_match(
526+
env,
527+
got,
528+
"pkg-0.0.1-py3-none-musllinux_1_4_x86_64.whl",
529+
)
530+
531+
_tests.append(_test_multiple_mvs_match_override_less_specific)
532+
437533
def _test_android(env):
438534
got = _select_whl(
439535
whls = [

0 commit comments

Comments
 (0)