-
Notifications
You must be signed in to change notification settings - Fork 1
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
Refactor manylinux tag logic #1
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,16 @@ | ||
import re | ||
import sys | ||
import warnings | ||
|
||
from pathlib import Path | ||
from posixpath import expandvars | ||
from typing import TYPE_CHECKING, Dict, List, Optional | ||
from urllib.parse import urldefrag, urlparse, urlsplit, urlunsplit | ||
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple | ||
from urllib.parse import urldefrag, urlsplit, urlunsplit | ||
|
||
from clikit.api.io.flags import VERY_VERBOSE | ||
from clikit.io import ConsoleIO, NullIO | ||
from packaging.tags import compatible_tags, cpython_tags, mac_platforms | ||
from packaging.version import Version | ||
|
||
from conda_lock.interfaces.vendored_poetry import ( | ||
Chooser, | ||
|
@@ -39,7 +41,6 @@ | |
if TYPE_CHECKING: | ||
from packaging.tags import Tag | ||
|
||
|
||
# NB: in principle these depend on the glibc on the machine creating the conda env. | ||
# We use tags supported by manylinux Docker images, which are likely the most common | ||
# in practice, see https://github.com/pypa/manylinux/blob/main/README.rst#docker-images. | ||
|
@@ -55,6 +56,12 @@ class PlatformEnv(Env): | |
Fake poetry Env to match PyPI distributions to the target conda environment | ||
""" | ||
|
||
_sys_platform: Literal["darwin", "linux", "win32"] | ||
_platform_system: Literal["Darwin", "Linux", "Windows"] | ||
_os_name: Literal["posix", "nt"] | ||
_platforms: List[str] | ||
_python_version: Tuple[int, ...] | ||
|
||
def __init__( | ||
self, | ||
python_version: str, | ||
|
@@ -67,33 +74,12 @@ def __init__( | |
arch = "x86_64" | ||
|
||
if system == "linux": | ||
# We use MANYLINUX_TAGS but only go up to the latest supported version | ||
# as provided by __glibc if present | ||
self._platforms = [] | ||
if platform_virtual_packages: | ||
# Get the glibc_version from virtual packages, falling back to | ||
# the last of MANYLINUX_TAGS when absent | ||
glibc_version = MANYLINUX_TAGS[-1] | ||
for p in platform_virtual_packages.values(): | ||
if p["name"] == "__glibc": | ||
glibc_version = p["version"] | ||
glibc_version_splits = list(map(int, glibc_version.split("."))) | ||
for tag in MANYLINUX_TAGS: | ||
if tag[0] == "_": | ||
# Compare to see if glibc_version is greater than this version | ||
if list(map(int, tag[1:].split("_"))) > glibc_version_splits: | ||
break | ||
self._platforms.append(f"manylinux{tag}_{arch}") | ||
if tag == glibc_version: # Catches 1 and 2010 case | ||
# We go no further than the maximum specific GLIBC version | ||
break | ||
# Latest tag is most preferred so list first | ||
self._platforms.reverse() | ||
else: | ||
self._platforms = [ | ||
f"manylinux{tag}_{arch}" for tag in reversed(MANYLINUX_TAGS) | ||
] | ||
self._platforms.append(f"linux_{arch}") | ||
compatible_manylinux_tags = _compute_compatible_manylinux_tags( | ||
platform_virtual_packages=platform_virtual_packages | ||
) | ||
self._platforms = [ | ||
f"manylinux{tag}_{arch}" for tag in compatible_manylinux_tags | ||
] + [f"linux_{arch}"] | ||
elif system == "osx": | ||
self._platforms = list(mac_platforms(MACOS_VERSION, arch)) | ||
elif platform == "win-64": | ||
|
@@ -140,6 +126,115 @@ def get_marker_env(self) -> Dict[str, str]: | |
} | ||
|
||
|
||
def _extract_glibc_version_from_virtual_packages( | ||
platform_virtual_packages: Dict[str, dict] | ||
) -> Optional[Version]: | ||
"""Get the glibc version from the "package" repodata of a chosen platform. | ||
|
||
Note that the glibc version coming from a virtual package is never a legacy | ||
manylinux tag (i.e. 1, 2010, or 2014). Those tags predate PEP 600 which | ||
introduced manylinux tags containing the glibc version. Currently, all | ||
relevant glibc versions look like 2.XX. | ||
|
||
>>> platform_virtual_packages = { | ||
... "__glibc-2.17-0.tar.bz2": { | ||
... "name": "__glibc", | ||
... "version": "2.17", | ||
... }, | ||
... } | ||
>>> _extract_glibc_version_from_virtual_packages(platform_virtual_packages) | ||
<Version('2.17')> | ||
>>> _extract_glibc_version_from_virtual_packages({}) is None | ||
True | ||
""" | ||
matches: List[Version] = [] | ||
for p in platform_virtual_packages.values(): | ||
if p["name"] == "__glibc": | ||
matches.append(Version(p["version"])) | ||
if len(matches) == 0: | ||
return None | ||
elif len(matches) == 1: | ||
return matches[0] | ||
else: | ||
lowest = min(matches) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will technically not work for those "special cases" because Version("2010") > Version("2.27") I suspect. I don't think this is a case that is very common though so maybe we can ignore it or error out? Or special case the special cases like you do below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to my understanding, there is no glibc v2010, so this should never happen. I made an attempt to explain this in the docstring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes, you are right, 2010 was actually the old manylinux name for 2.12 so yes, the glibc version would be 2.12. |
||
warnings.warn( | ||
f"Multiple __glibc virtual package entries found! " | ||
f"{matches=} Using the lowest version {lowest}." | ||
) | ||
return lowest | ||
|
||
|
||
def _glibc_version_from_manylinux_tag(tag: str) -> Version: | ||
""" | ||
Return the glibc version for the given manylinux tag | ||
|
||
>>> _glibc_version_from_manylinux_tag("2010") | ||
<Version('2.12')> | ||
>>> _glibc_version_from_manylinux_tag("_2_28") | ||
<Version('2.28')> | ||
""" | ||
SPECIAL_CASES = { | ||
"1": Version("2.5"), | ||
"2010": Version("2.12"), | ||
"2014": Version("2.17"), | ||
} | ||
if tag in SPECIAL_CASES: | ||
return SPECIAL_CASES[tag] | ||
elif tag.startswith("_"): | ||
return Version(tag[1:].replace("_", ".")) | ||
else: | ||
raise ValueError(f"Unknown manylinux tag {tag}") | ||
|
||
|
||
def _compute_compatible_manylinux_tags( | ||
platform_virtual_packages: Optional[Dict[str, dict]] | ||
) -> List[str]: | ||
"""Determine the manylinux tags that are compatible with the given platform. | ||
|
||
If there is no glibc virtual package, then assume that all manylinux tags are | ||
compatible. | ||
|
||
The result is sorted in descending order in order to favor the latest. | ||
|
||
>>> platform_virtual_packages = { | ||
... "__glibc-2.24-0.tar.bz2": { | ||
... "name": "__glibc", | ||
... "version": "2.24", | ||
... }, | ||
... } | ||
>>> _compute_compatible_manylinux_tags({}) == list(reversed(MANYLINUX_TAGS)) | ||
True | ||
>>> _compute_compatible_manylinux_tags(platform_virtual_packages) | ||
['_2_24', '_2_17', '2014', '2010', '1'] | ||
""" | ||
# We use MANYLINUX_TAGS but only go up to the latest supported version | ||
# as provided by __glibc if present | ||
|
||
latest_supported_glibc_version: Optional[Version] = None | ||
# Try to get the glibc version from the virtual packages if it exists | ||
if platform_virtual_packages: | ||
latest_supported_glibc_version = _extract_glibc_version_from_virtual_packages( | ||
platform_virtual_packages | ||
) | ||
# Fall back to the latest of MANYLINUX_TAGS | ||
if latest_supported_glibc_version is None: | ||
latest_supported_glibc_version = _glibc_version_from_manylinux_tag( | ||
MANYLINUX_TAGS[-1] | ||
) | ||
|
||
# The glibc versions are backwards compatible, so filter the MANYLINUX_TAGS | ||
# to those compatible with less than or equal to the latest supported | ||
# glibc version. | ||
# Note that MANYLINUX_TAGS is sorted in ascending order. The latest tag | ||
# is most preferred so we reverse the order. | ||
compatible_manylinux_tags = [ | ||
tag | ||
for tag in reversed(MANYLINUX_TAGS) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small nit: there is a tad of a wasted thing here because once one matches, all the rest will match. I don't think it has a huge consequence though and this code is definitely easier to follow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean that we could break the iteration early? Technically yes, but that's more of a nanosecond optimization, right? |
||
if _glibc_version_from_manylinux_tag(tag) <= latest_supported_glibc_version | ||
] | ||
return compatible_manylinux_tags | ||
|
||
|
||
REQUIREMENT_PATTERN = re.compile( | ||
r""" | ||
^ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a type annotation, so if you're running a type checker like
mypy
orpyright
it will know what are the valid values of_sys_platform
. I find it really useful because if you're in another file and you hover over a variable it'll show you the type and hence the possible values, making it much easier to figure out what's going on without needing to infer it from reading the code.So in this case I was reading the code, I figured out that these are the possible values, and I declared them as such. And now the type checker statically verifies this and can offer hints.