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

Ability to install armv7l manylinux/musllinux wheels on armv8l #690

Merged
merged 3 commits into from Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 35 additions & 23 deletions src/packaging/_manylinux.py
Expand Up @@ -5,7 +5,7 @@
import re
import sys
import warnings
from typing import Dict, Generator, Iterator, NamedTuple, Optional, Tuple
from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple

from ._elffile import EIClass, EIData, ELFFile, EMachine

Expand Down Expand Up @@ -50,12 +50,13 @@ def _is_linux_i686(executable: str) -> bool:
)


def _have_compatible_abi(executable: str, arch: str) -> bool:
if arch == "armv7l":
def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
if "armv7l" in archs:
return _is_linux_armhf(executable)
if arch == "i686":
if "i686" in archs:
return _is_linux_i686(executable)
return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x", "loongarch64"}
allowed_archs = {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x", "loongarch64"}
return any(arch in allowed_archs for arch in archs)


# If glibc ever changes its major version, we need to know what the last
Expand Down Expand Up @@ -203,12 +204,22 @@ def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
}


def platform_tags(linux: str, arch: str) -> Iterator[str]:
if not _have_compatible_abi(sys.executable, arch):
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate manylinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be manylinux-compatible.
:returns: An iterator of compatible manylinux tags.
"""
if not _have_compatible_abi(sys.executable, archs):
return
# Oldest glibc to be supported regardless of architecture is (2, 17).
too_old_glibc2 = _GLibCVersion(2, 16)
if arch in {"x86_64", "i686"}:
if set(archs) & {"x86_64", "i686"}:
# On x86/i686 also oldest glibc to be supported is (2, 5).
too_old_glibc2 = _GLibCVersion(2, 4)
current_glibc = _GLibCVersion(*_get_glibc_version())
Expand All @@ -222,19 +233,20 @@ def platform_tags(linux: str, arch: str) -> Iterator[str]:
for glibc_major in range(current_glibc.major - 1, 1, -1):
glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, glibc_version):
yield linux.replace("linux", tag)
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
for arch in archs:
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, glibc_version):
yield linux.replace("linux", legacy_tag)
yield f"{tag}_{arch}"
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(arch, glibc_version):
yield f"{legacy_tag}_{arch}"
17 changes: 10 additions & 7 deletions src/packaging/_musllinux.py
Expand Up @@ -8,7 +8,7 @@
import re
import subprocess
import sys
from typing import Iterator, NamedTuple, Optional
from typing import Iterator, NamedTuple, Optional, Sequence

from ._elffile import ELFFile

Expand Down Expand Up @@ -51,20 +51,23 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
return _parse_musl_version(proc.stderr)


def platform_tags(arch: str) -> Iterator[str]:
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate musllinux tags compatible to the current platform.
:param arch: Should be the part of platform tag after the ``linux_``
prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a
prerequisite for the current platform to be musllinux-compatible.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be musllinux-compatible.
:returns: An iterator of compatible musllinux tags.
"""
sys_musl = _get_musl_version(sys.executable)
if sys_musl is None: # Python not dynamically linked against musl.
return
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
for arch in archs:
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"


if __name__ == "__main__": # pragma: no cover
Expand Down
14 changes: 10 additions & 4 deletions src/packaging/tags.py
Expand Up @@ -469,15 +469,21 @@ def mac_platforms(

def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
linux = _normalize_string(sysconfig.get_platform())
if not linux.startswith("linux_"):
# we should never be here, just yield the sysconfig one and return
yield linux
return
if is_32bit:
if linux == "linux_x86_64":
linux = "linux_i686"
elif linux == "linux_aarch64":
linux = "linux_armv7l"
linux = "linux_armv8l"
brettcannon marked this conversation as resolved.
Show resolved Hide resolved
_, arch = linux.split("_", 1)
yield from _manylinux.platform_tags(linux, arch)
yield from _musllinux.platform_tags(arch)
yield linux
archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch])
yield from _manylinux.platform_tags(archs)
yield from _musllinux.platform_tags(archs)
for arch in archs:
yield f"linux_{arch}"


def _generic_platforms() -> Iterator[str]:
Expand Down
50 changes: 37 additions & 13 deletions tests/test_tags.py
Expand Up @@ -360,10 +360,10 @@ def test_get_config_var_does_log(self, monkeypatch):
@pytest.mark.parametrize(
"arch,is_32bit,expected",
[
("linux-x86_64", False, "linux_x86_64"),
("linux-x86_64", True, "linux_i686"),
("linux-aarch64", False, "linux_aarch64"),
("linux-aarch64", True, "linux_armv7l"),
("linux-x86_64", False, ["linux_x86_64"]),
("linux-x86_64", True, ["linux_i686"]),
("linux-aarch64", False, ["linux_aarch64"]),
("linux-aarch64", True, ["linux_armv8l", "linux_armv7l"]),
],
)
def test_linux_platforms_32_64bit_on_64bit_os(
Expand All @@ -372,7 +372,9 @@ def test_linux_platforms_32_64bit_on_64bit_os(
monkeypatch.setattr(sysconfig, "get_platform", lambda: arch)
monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.20", raising=False)
monkeypatch.setattr(tags._manylinux, "_is_compatible", lambda *args: False)
linux_platform = list(tags._linux_platforms(is_32bit=is_32bit))[-1]
linux_platform = list(tags._linux_platforms(is_32bit=is_32bit))[
-len(expected) :
]
assert linux_platform == expected

def test_linux_platforms_manylinux_unsupported(self, monkeypatch):
Expand Down Expand Up @@ -445,14 +447,20 @@ def test_linux_platforms_manylinux2014(self, monkeypatch):
]
assert platforms == expected

def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch):
@pytest.mark.parametrize(
"native_arch, cross_arch",
[("armv7l", "armv7l"), ("armv8l", "armv8l"), ("aarch64", "armv8l")],
)
def test_linux_platforms_manylinux2014_armhf_abi(
self, native_arch, cross_arch, monkeypatch
):
monkeypatch.setattr(tags._manylinux, "_glibc_version_string", lambda: "2.30")
monkeypatch.setattr(
tags._manylinux,
"_is_compatible",
lambda _, glibc_version: glibc_version == _GLibCVersion(2, 17),
)
monkeypatch.setattr(sysconfig, "get_platform", lambda: "linux_armv7l")
monkeypatch.setattr(sysconfig, "get_platform", lambda: f"linux_{native_arch}")
monkeypatch.setattr(
sys,
"executable",
Expand All @@ -463,7 +471,11 @@ def test_linux_platforms_manylinux2014_armhf_abi(self, monkeypatch):
),
)
platforms = list(tags._linux_platforms(is_32bit=True))
expected = ["manylinux_2_17_armv7l", "manylinux2014_armv7l", "linux_armv7l"]
archs = {"armv8l": ["armv8l", "armv7l"]}.get(cross_arch, [cross_arch])
expected = []
for arch in archs:
expected.extend([f"manylinux_2_17_{arch}", f"manylinux2014_{arch}"])
expected.extend(f"linux_{arch}" for arch in archs)
assert platforms == expected

def test_linux_platforms_manylinux2014_i386_abi(self, monkeypatch):
Expand Down Expand Up @@ -525,7 +537,8 @@ def test_linux_platforms_manylinux_glibc3(self, monkeypatch):
@pytest.mark.parametrize(
"native_arch, cross32_arch, musl_version",
[
("aarch64", "armv7l", _MuslVersion(1, 1)),
("armv7l", "armv7l", _MuslVersion(1, 1)),
("aarch64", "armv8l", _MuslVersion(1, 1)),
("i386", "i386", _MuslVersion(1, 2)),
("x86_64", "i686", _MuslVersion(1, 2)),
],
Expand All @@ -548,10 +561,14 @@ def test_linux_platforms_musllinux(

platforms = list(tags._linux_platforms(is_32bit=cross32))
target_arch = cross32_arch if cross32 else native_arch
expected = [
f"musllinux_{musl_version[0]}_{minor}_{target_arch}"
for minor in range(musl_version[1], -1, -1)
] + [f"linux_{target_arch}"]
archs = {"armv8l": ["armv8l", "armv7l"]}.get(target_arch, [target_arch])
expected = []
for arch in archs:
expected.extend(
f"musllinux_{musl_version[0]}_{minor}_{arch}"
for minor in range(musl_version[1], -1, -1)
)
expected.extend(f"linux_{arch}" for arch in archs)
assert platforms == expected

assert recorder.calls == [pretend.call(fake_executable)]
Expand Down Expand Up @@ -590,6 +607,13 @@ def test_linux_platforms_not_manylinux_abi(
expected = [f"linux_{alt_machine}"]
assert platforms == expected

def test_linux_not_linux(self, monkeypatch):
monkeypatch.setattr(sysconfig, "get_platform", lambda: "not_linux_x86_64")
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
monkeypatch.setattr(os, "confstr", lambda x: "glibc 2.17", raising=False)
platforms = list(tags._linux_platforms(is_32bit=False))
assert platforms == ["not_linux_x86_64"]


@pytest.mark.parametrize(
"platform_name,dispatch_func",
Expand Down