Skip to content

Commit b7f3313

Browse files
authored
Merge branch 'main' into fix.site.packages.overlap
2 parents ffdd2de + 5fa1a87 commit b7f3313

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
* (venvs) {obj}`--vens_site_packages=yes` no longer errors when packages with
8390
overlapping files or directories are used together.
8491
([#3204](https://github.com/bazel-contrib/rules_python/issues/3204)).

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)