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

Preliminary manylinux2 support. #5008

Closed
wants to merge 2 commits into from
Closed
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
3 changes: 3 additions & 0 deletions news/5008.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Implement manylinux2 platform tag support. manylinux2 is the successor
to manylinux1. It allows carefully compiled binary wheels to be installed
on compatible Linux platforms.
29 changes: 27 additions & 2 deletions src/pip/_internal/pep425tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@ def is_manylinux1_compatible():
return pip._internal.utils.glibc.have_compatible_glibc(2, 5)


def is_manylinux2_compatible():
# Only Linux, and only x86-64 / i686
if get_platform() not in {"linux_x86_64", "linux_i686"}:
return False

# Check for presence of _manylinux module
try:
import _manylinux
return bool(_manylinux.manylinux2_compatible)
except (ImportError, AttributeError):
# Fall through to heuristic check below
pass

# Check glibc version. CentOS 6 uses glibc 2.12.
return pip._internal.utils.glibc.have_compatible_glibc(2, 12)


def get_darwin_arches(major, minor, machine):
"""Return a list of supported arches (including group arches) for
the given major, minor and machine architecture of an macOS machine.
Expand Down Expand Up @@ -276,8 +293,16 @@ def get_supported(versions=None, noarch=False, platform=None,
else:
# arch pattern didn't match (?!)
arches = [arch]
elif platform is None and is_manylinux1_compatible():
arches = [arch.replace('linux', 'manylinux1'), arch]
elif arch.startswith('manylinux2'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I feel like this is probably a bit of footgun. It feels like the kind of thing that's asking to be a bug once we have a manylinux2018 or something.

# manylinux1 wheels run on manylinux2 systems.
Copy link
Member

@ncoghlan ncoghlan Apr 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the removal of ncurses 5 from the ABI requirement, this implication isn't necessarily true: it's possible for a platform to be compatible with manylinux2010 without being compatible with manylinux1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice pip's heuristics for deciding whether a platform is compatible with manylinux1 and manylinux2010 don't actually do any checking of ncurses. I suppose it's possible for someone to provide a _manylinux module that disables manylinux1 but enables manylinux2010 (though this would be very unusual).

The branch below that just calls is_manylinux2_compatible and is_manylinux1_compatible seems like the simplest approach, and it's obviously correct. In fact I don't even understand how this branch could get hit in the first place...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@njsmith The _manylinux.is_manylinux1_compatible == False override is exactly the case I had in mind. While that's unusual now, and while formally incompatible distros may choose not to set it on the basis of "ncurses dependencies are rare, while manylinux1 wheels are common", I think we want to start out with the position that both variants of Linux platform compatibility should be checked independently.

As for how this branch can be reached, it's only when manylinux2010 is passed in specifically as the platform argument (presumably because the system checking the tags isn't actually a Linux system, or else isn't manylinux compatible).

So I think the best way to start out will be with a strict interpretation of the manylinux2010 compatibility tag, and then treat "Opt-in to accepting manylinux1 wheels when explicitly specifying a manylinux2010 target" as a follow-on feature request.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wtolson pointed out that the PEP explicitly calls for manylinux1 wheels to be treated as manylinux2010 compatible, even with the ncurses dependency issue: https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels

I don't think it's worth going back and amending the PEP at this point, so let's just keep the branch, and add a reference to that part of the PEP in a comment.

arches = [arch, arch.replace('manylinux2', 'manylinux1')]
elif platform is None:
arches = []
if is_manylinux2_compatible():
arches.append(arch.replace('linux', 'manylinux2'))
if is_manylinux1_compatible():
arches.append(arch.replace('linux', 'manylinux1'))
arches.append(arch)
else:
arches = [arch]

Expand Down
113 changes: 65 additions & 48 deletions tests/functional/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,54 +322,71 @@ def test_download_specify_platform(script, data):
)


def test_download_platform_manylinux(script, data):
"""
Test using "pip download --platform" to download a .whl archive
supported for a specific platform.
"""
fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
# Confirm that universal wheels are returned even for specific
# platforms.
result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--platform', 'linux_x86_64',
'fake',
)
assert (
Path('scratch') / 'fake-1.0-py2.py3-none-any.whl'
in result.files_created
)

data.reset()
fake_wheel(data, 'fake-1.0-py2.py3-none-manylinux1_x86_64.whl')
result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--platform', 'manylinux1_x86_64',
'fake',
)
assert (
Path('scratch') /
'fake-1.0-py2.py3-none-manylinux1_x86_64.whl'
in result.files_created
)

# When specifying the platform, manylinux1 needs to be the
# explicit platform--it won't ever be added to the compatible
# tags.
data.reset()
fake_wheel(data, 'fake-1.0-py2.py3-none-linux_x86_64.whl')
result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--platform', 'linux_x86_64',
'fake',
expect_error=True,
)
class TestDownloadPlatformManylinuxes(object):
"""
"pip download --platform" downloads a .whl archive supported for
manylinux platforms.
"""

@pytest.mark.parametrize("platform", [
"linux_x86_64",
"manylinux1_x86_64",
"manylinux2_x86_64",
])
def test_download_universal(self, platform, script, data):
"""
Universal wheels are returned even for specific platforms.
"""
fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--platform', platform,
'fake',
)
assert (
Path('scratch') / 'fake-1.0-py2.py3-none-any.whl'
in result.files_created
)

@pytest.mark.parametrize("wheel_abi,platform", [
("manylinux1_x86_64", "manylinux1_x86_64"),
("manylinux1_x86_64", "manylinux2_x86_64"),
("manylinux2_x86_64", "manylinux2_x86_64"),
])
def test_download_compatible_manylinuxes(
self, wheel_abi, platform, script, data,
):
"""
Earlier manylinuxes are compatible with later manylinuxes.
"""
wheel = 'fake-1.0-py2.py3-none-{}.whl'.format(wheel_abi)
fake_wheel(data, wheel)
result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--platform', platform,
'fake',
)
assert Path('scratch') / wheel in result.files_created

def test_explicit_platform_only(self, data, script):
"""
When specifying the platform, manylinux1 needs to be the
explicit platform--it won't ever be added to the compatible
tags.
"""
fake_wheel(data, 'fake-1.0-py2.py3-none-linux_x86_64.whl')
script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--platform', 'linux_x86_64',
'fake',
expect_error=True,
)


def test_download_specify_python_version(script, data):
Expand Down
83 changes: 72 additions & 11 deletions tests/unit/test_pep425tags.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys

import pytest

from mock import patch

from pip._internal import pep425tags
Expand Down Expand Up @@ -114,44 +116,57 @@ def test_manual_abi_dm_flags(self):
self.abi_tag_unicode('dm', {'Py_DEBUG': True, 'WITH_PYMALLOC': True})


class TestManylinux1Tags(object):

@pytest.mark.parametrize('is_manylinux_compatible', [
pep425tags.is_manylinux1_compatible,
pep425tags.is_manylinux2_compatible,
])
class TestManylinuxTags(object):
"""
Tests common to all manylinux tags (e.g. manylinux1, manylinux2,
...)
"""
@patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64')
@patch('pip._internal.utils.glibc.have_compatible_glibc',
lambda major, minor: True)
def test_manylinux1_compatible_on_linux_x86_64(self):
def test_manylinux_compatible_on_linux_x86_64(self,
is_manylinux_compatible):
"""
Test that manylinux1 is enabled on linux_x86_64
Test that manylinuxes are enabled on linux_x86_64
"""
assert pep425tags.is_manylinux1_compatible()
assert is_manylinux_compatible()

@patch('pip._internal.pep425tags.get_platform', lambda: 'linux_i686')
@patch('pip._internal.utils.glibc.have_compatible_glibc',
lambda major, minor: True)
def test_manylinux1_compatible_on_linux_i686(self):
def test_manylinux1_compatible_on_linux_i686(self,
is_manylinux_compatible):
"""
Test that manylinux1 is enabled on linux_i686
"""
assert pep425tags.is_manylinux1_compatible()
assert is_manylinux_compatible()

@patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64')
@patch('pip._internal.utils.glibc.have_compatible_glibc',
lambda major, minor: False)
def test_manylinux1_2(self):
def test_manylinux1_2(self, is_manylinux_compatible):
"""
Test that manylinux1 is disabled with incompatible glibc
"""
assert not pep425tags.is_manylinux1_compatible()
assert not is_manylinux_compatible()

@patch('pip._internal.pep425tags.get_platform', lambda: 'arm6vl')
@patch('pip._internal.utils.glibc.have_compatible_glibc',
lambda major, minor: True)
def test_manylinux1_3(self):
def test_manylinux1_3(self, is_manylinux_compatible):
"""
Test that manylinux1 is disabled on arm6vl
"""
assert not pep425tags.is_manylinux1_compatible()
assert not is_manylinux_compatible()


class TestManylinux1Tags(object):

@patch('pip._internal.pep425tags.is_manylinux2_compatible', lambda: False)
@patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64')
@patch('pip._internal.utils.glibc.have_compatible_glibc',
lambda major, minor: True)
Expand All @@ -172,3 +187,49 @@ def test_manylinux1_tag_is_first(self):
assert arches == ['manylinux1_x86_64', 'linux_x86_64', 'any']
else:
assert arches == ['manylinux1_x86_64', 'linux_x86_64']


class TestManylinux2Tags(object):

@patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64')
@patch('pip._internal.utils.glibc.have_compatible_glibc',
lambda major, minor: True)
@patch('sys.platform', 'linux2')
def test_manylinux2_tag_is_first(self):
"""
Test that the more specific tag manylinux2 comes first.
"""
groups = {}
for pyimpl, abi, arch in pep425tags.get_supported():
groups.setdefault((pyimpl, abi), []).append(arch)

for arches in groups.values():
if arches == ['any']:
continue
# Expect the most specific arch first:
if len(arches) == 4:
assert arches == ['manylinux2_x86_64',
'manylinux1_x86_64',
'linux_x86_64',
'any']
else:
assert arches == ['manylinux2_x86_64',
'manylinux1_x86_64',
'linux_x86_64']

@pytest.mark.parametrize("manylinux2,manylinux1", [
("manylinux2_x86_64", "manylinux1_x86_64"),
("manylinux2_i686", "manylinux1_i686"),
])
def test_manylinux2_implies_manylinux1(self, manylinux2, manylinux1):
"""
Specifying manylinux2 implies manylinux1.
"""
groups = {}
for pyimpl, abi, arch in pep425tags.get_supported(platform=manylinux2):
groups.setdefault((pyimpl, abi), []).append(arch)

for arches in groups.values():
if arches == ['any']:
continue
assert arches[:2] == [manylinux2, manylinux1]
2 changes: 1 addition & 1 deletion tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ def test_create_and_cleanup_work(self):


class TestGlibc(object):
def test_manylinux1_check_glibc_version(self):
def test_manylinux_check_glibc_version(self):
"""
Test that the check_glibc_version function is robust against weird
glibc version strings.
Expand Down