Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle compatible libc-based manylinux/musllinux platform tags #10894

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions news/10760.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Support :pep:`600` and :pep:`656` to install and download
manylinux and musllinux compatible wheels when ``--platform`` is specified.
30 changes: 28 additions & 2 deletions src/pip/_internal/utils/compatibility_tags.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""Generate and work with PEP 425 Compatibility Tags.
"""

import re
from typing import List, Optional, Tuple
from typing import Generator, List, Optional, Tuple

from pip._vendor.packaging.tags import (
PythonVersion,
Expand All @@ -13,10 +12,17 @@
interpreter_name,
interpreter_version,
mac_platforms,
platform_tags,
)

_osx_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")

_LEGACY_MANYLINUX_MAP = {
"manylinux2014": (2, 17),
"manylinux2010": (2, 12),
"manylinux1": (2, 5),
}


def version_info_to_nodot(version_info: Tuple[int, ...]) -> str:
# Only use up to the first two numbers.
Expand All @@ -43,6 +49,22 @@ def _mac_platforms(arch: str) -> List[str]:
return arches


def filter_libc_tags(libc: Tuple[int, int], arch: str) -> Generator[str, None, None]:
for tag in filter(
lambda t: t.startswith(("manylinux", "musllinux")), platform_tags()
):
tag_prefix, _, tag_suffix = tag.partition("_")
if tag_prefix in _LEGACY_MANYLINUX_MAP:
tag_libc = _LEGACY_MANYLINUX_MAP[tag_prefix]
tag_arch = tag_suffix
else:
tag_libc_major, tag_libc_minor, tag_arch = tag_suffix.split("_", 2)
tag_libc = (int(tag_libc_major), int(tag_libc_minor))

if arch == tag_arch and tag_libc <= libc:
yield tag


def _custom_manylinux_platforms(arch: str) -> List[str]:
arches = [arch]
arch_prefix, arch_sep, arch_suffix = arch.partition("_")
Expand Down Expand Up @@ -70,6 +92,10 @@ def _get_custom_platforms(arch: str) -> List[str]:
arches = _mac_platforms(arch)
elif arch_prefix in ["manylinux2014", "manylinux2010"]:
arches = _custom_manylinux_platforms(arch)
elif arch_prefix in ["manylinux", "musllinux"]:
curr_libc_major, curr_libc_minor, curr_arch = arch_suffix.split("_", 2)
curr_libc = (int(curr_libc_major), int(curr_libc_minor))
arches = list(filter_libc_tags(curr_libc, curr_arch)) or [arch]
else:
arches = [arch]
return arches
Expand Down
250 changes: 249 additions & 1 deletion tests/unit/test_utils_compatibility_tags.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import os
import platform
import sys
import sysconfig
from typing import Any, Callable, Dict, List, Tuple
import types
from typing import Any, Callable, Dict, List, Optional, Tuple
from unittest.mock import patch

import pytest
from pip._vendor.packaging.tags import _manylinux, _musllinux

from pip._internal.utils import compatibility_tags

ManylinuxModule = Callable[[pytest.MonkeyPatch], types.ModuleType]


@pytest.fixture
def manylinux_module(monkeypatch: pytest.MonkeyPatch) -> types.ModuleType:
monkeypatch.setattr(_manylinux, "_get_glibc_version", lambda *args: (2, 20))
module_name = "_manylinux"
module = types.ModuleType(module_name)
monkeypatch.setitem(sys.modules, module_name, module)
return module


@pytest.mark.parametrize(
"version_info, expected",
Expand Down Expand Up @@ -56,6 +72,196 @@ def test_no_hyphen_tag(self) -> None:
assert "-" not in tag.platform


class TestManylinuxTags:
def teardown_method(self) -> None:
_manylinux._get_glibc_version.cache_clear()

@pytest.mark.parametrize(
"manylinux,expected,glibc_ver",
[
(
"manylinux_2_12_x86_64",
[
"manylinux_2_12_x86_64",
"manylinux2010_x86_64",
"manylinux_2_11_x86_64",
"manylinux_2_10_x86_64",
"manylinux_2_9_x86_64",
"manylinux_2_8_x86_64",
"manylinux_2_7_x86_64",
"manylinux_2_6_x86_64",
"manylinux_2_5_x86_64",
"manylinux1_x86_64",
],
"2.12",
),
(
"manylinux_2_17_x86_64",
[
"manylinux_2_17_x86_64",
"manylinux2014_x86_64",
"manylinux_2_16_x86_64",
"manylinux_2_15_x86_64",
"manylinux_2_14_x86_64",
"manylinux_2_13_x86_64",
"manylinux_2_12_x86_64",
"manylinux2010_x86_64",
"manylinux_2_11_x86_64",
"manylinux_2_10_x86_64",
"manylinux_2_9_x86_64",
"manylinux_2_8_x86_64",
"manylinux_2_7_x86_64",
"manylinux_2_6_x86_64",
"manylinux_2_5_x86_64",
"manylinux1_x86_64",
],
"2.17",
),
(
"manylinux_2_5_x86_64",
[
"manylinux_2_5_x86_64",
"manylinux1_x86_64",
],
"2.5",
),
(
"manylinux_2_17_s390x",
[
"manylinux_2_17_s390x",
"manylinux2014_s390x",
],
"2.17",
),
(
"manylinux_2_17_x86_64",
[
"manylinux_2_12_x86_64",
"manylinux2010_x86_64",
"manylinux_2_11_x86_64",
"manylinux_2_10_x86_64",
"manylinux_2_9_x86_64",
"manylinux_2_8_x86_64",
"manylinux_2_7_x86_64",
"manylinux_2_6_x86_64",
"manylinux_2_5_x86_64",
"manylinux1_x86_64",
],
"2.12",
),
],
)
def test_manylinux(
self,
monkeypatch: pytest.MonkeyPatch,
manylinux: str,
expected: List[str],
glibc_ver: str,
) -> None:
*_, arch = manylinux.split("_", 3)
monkeypatch.setattr(platform, "system", lambda: "Linux")
monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{arch}")
monkeypatch.setattr(platform, "machine", lambda: arch)
monkeypatch.setattr(
os, "confstr", lambda x: f"glibc {glibc_ver}", raising=False
)
groups: Dict[Tuple[str, str], List[str]] = {}
supported = compatibility_tags.get_supported(platforms=[manylinux])
for tag in supported:
groups.setdefault((tag.interpreter, tag.abi), []).append(tag.platform)

for arches in groups.values():
if "any" in arches:
continue
assert arches == expected


class TestManylinuxCompatibleTags:
@pytest.mark.parametrize(
"machine, major, minor, tf", [("x86_64", 2, 20, False), ("s390x", 2, 22, True)]
)
def test_use_manylinux_compatible(
self,
monkeypatch: pytest.MonkeyPatch,
manylinux_module: ManylinuxModule,
machine: str,
major: int,
minor: int,
tf: bool,
) -> None:
def manylinux_compatible(tag_major: int, tag_minor: int, tag_arch: str) -> bool:
if tag_major == 2 and tag_minor == 22:
return tag_arch == "s390x"
return False

monkeypatch.setattr(platform, "system", lambda: "Linux")
monkeypatch.setattr(
_manylinux,
"_get_glibc_version",
lambda: (major, minor),
)
monkeypatch.setattr(
manylinux_module,
"manylinux_compatible",
manylinux_compatible,
raising=False,
)
groups: Dict[Tuple[str, str], List[str]] = {}
manylinux = f"manylinux_{major}_{minor}_{machine}"
supported = compatibility_tags.get_supported(platforms=[manylinux])
for tag in supported:
groups.setdefault((tag.interpreter, tag.abi), []).append(tag.platform)

if tf:
expected = [f"manylinux_2_22_{machine}"]
else:
expected = [f"manylinux_{major}_{minor}_{machine}"]
for arches in groups.values():
if "any" in arches:
continue
assert arches == expected

def test_linux_use_manylinux_compatible_none(
self, monkeypatch: pytest.MonkeyPatch, manylinux_module: ManylinuxModule
) -> None:
def manylinux_compatible(
tag_major: int, tag_minor: int, tag_arch: str
) -> Optional[bool]:
if tag_major == 2 and tag_minor < 25:
return False
return None

monkeypatch.setattr(platform, "system", lambda: "Linux")
monkeypatch.setattr(_manylinux, "_get_glibc_version", lambda: (2, 30))
monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_x86_64")
monkeypatch.setattr(
manylinux_module,
"manylinux_compatible",
manylinux_compatible,
raising=False,
)

groups: Dict[Tuple[str, str], List[str]] = {}
supported = compatibility_tags.get_supported(
platforms=["manylinux_2_30_x86_64"]
)
for tag in supported:
groups.setdefault((tag.interpreter, tag.abi), []).append(tag.platform)

expected = [
"manylinux_2_30_x86_64",
"manylinux_2_29_x86_64",
"manylinux_2_28_x86_64",
"manylinux_2_27_x86_64",
"manylinux_2_26_x86_64",
"manylinux_2_25_x86_64",
]
for arches in groups.values():
if "any" in arches:
continue
assert arches == expected


class TestManylinux2010Tags:
@pytest.mark.parametrize(
"manylinux2010,manylinux1",
Expand Down Expand Up @@ -106,3 +312,45 @@ def test_manylinuxA_implies_manylinuxB(
if arches == ["any"]:
continue
assert arches[:3] == expected_arches


class TestMusllinuxTags:
@pytest.mark.parametrize(
"musllinux,arch,musl_ver",
[
("musllinux_1_4", "x86_64", (1, 4)),
("musllinux_1_4", "i686", (1, 2)),
],
)
def test_musllinux(
self,
monkeypatch: pytest.MonkeyPatch,
musllinux: str,
arch: str,
musl_ver: Tuple[int, int],
) -> None:
monkeypatch.setattr(
_manylinux,
"_get_glibc_version",
lambda: (-1, -1),
)
monkeypatch.setattr(
_musllinux,
"_get_musl_version",
lambda _: _musllinux._MuslVersion(*musl_ver),
)
monkeypatch.setattr(platform, "system", lambda: "Linux")
monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{arch}")
groups: Dict[Tuple[str, str], List[str]] = {}
supported = compatibility_tags.get_supported(platforms=[f"{musllinux}_{arch}"])
for tag in supported:
groups.setdefault((tag.interpreter, tag.abi), []).append(tag.platform)

expected = [
f"musllinux_{musl_ver[0]}_{minor}_{arch}"
for minor in range(musl_ver[1], -1, -1)
]
for arches in groups.values():
if "any" in arches:
continue
assert arches == expected