diff --git a/crates/bench/benches/uv.rs b/crates/bench/benches/uv.rs index ac51efcc7388..b7d3accee59a 100644 --- a/crates/bench/benches/uv.rs +++ b/crates/bench/benches/uv.rs @@ -127,8 +127,9 @@ mod resolver { Arch::Aarch64, ); - static TAGS: LazyLock = - LazyLock::new(|| Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false).unwrap()); + static TAGS: LazyLock = LazyLock::new(|| { + Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false).unwrap() + }); pub(crate) async fn resolve( manifest: Manifest, diff --git a/crates/platform-tags/src/tags.rs b/crates/platform-tags/src/tags.rs index e7e04c0f9987..67ab80357cdf 100644 --- a/crates/platform-tags/src/tags.rs +++ b/crates/platform-tags/src/tags.rs @@ -94,10 +94,19 @@ impl Tags { python_version: (u8, u8), implementation_name: &str, implementation_version: (u8, u8), + manylinux_compatible: bool, gil_disabled: bool, ) -> Result { let implementation = Implementation::parse(implementation_name, gil_disabled)?; - let platform_tags = compatible_tags(platform)?; + + // Determine the compatible tags for the current platform. + let platform_tags = { + let mut platform_tags = compatible_tags(platform)?; + if matches!(platform.os(), Os::Manylinux { .. }) && !manylinux_compatible { + platform_tags.retain(|tag| !tag.starts_with("manylinux")); + } + platform_tags + }; let mut tags = Vec::with_capacity(5 * platform_tags.len()); @@ -931,6 +940,64 @@ mod tests { ); } + /// Ensure the tags returned do not include the `manylinux` tags + /// when `manylinux_incompatible` is set to `false`. + #[test] + fn test_manylinux_incompatible() { + let tags = Tags::from_env( + &Platform::new( + Os::Manylinux { + major: 2, + minor: 28, + }, + Arch::X86_64, + ), + (3, 9), + "cpython", + (3, 9), + false, + false, + ) + .unwrap(); + assert_snapshot!( + tags, + @r###" + cp39-cp39-linux_x86_64 + cp39-abi3-linux_x86_64 + cp39-none-linux_x86_64 + cp38-abi3-linux_x86_64 + cp37-abi3-linux_x86_64 + cp36-abi3-linux_x86_64 + cp35-abi3-linux_x86_64 + cp34-abi3-linux_x86_64 + cp33-abi3-linux_x86_64 + cp32-abi3-linux_x86_64 + py39-none-linux_x86_64 + py3-none-linux_x86_64 + py38-none-linux_x86_64 + py37-none-linux_x86_64 + py36-none-linux_x86_64 + py35-none-linux_x86_64 + py34-none-linux_x86_64 + py33-none-linux_x86_64 + py32-none-linux_x86_64 + py31-none-linux_x86_64 + py30-none-linux_x86_64 + cp39-none-any + py39-none-any + py3-none-any + py38-none-any + py37-none-any + py36-none-any + py35-none-any + py34-none-any + py33-none-any + py32-none-any + py31-none-any + py30-none-any + "###); + } + /// Check full tag ordering. /// The list is displayed in decreasing priority. /// @@ -951,6 +1018,7 @@ mod tests { (3, 9), "cpython", (3, 9), + true, false, ) .unwrap(); @@ -1575,6 +1643,7 @@ mod tests { "cpython", (3, 9), false, + false, ) .unwrap(); assert_snapshot!( diff --git a/crates/uv-python/python/get_interpreter_info.py b/crates/uv-python/python/get_interpreter_info.py index 14fb6635e29e..797b53b4d688 100644 --- a/crates/uv-python/python/get_interpreter_info.py +++ b/crates/uv-python/python/get_interpreter_info.py @@ -542,6 +542,17 @@ def main() -> None: "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, } + os_and_arch = get_operating_system_and_architecture() + + manylinux_compatible = True + if os_and_arch["os"]["name"] == "manylinux": + # noinspection PyProtectedMember + from .packaging._manylinux import _get_glibc_version, _is_compatible + + manylinux_compatible = _is_compatible( + arch=os_and_arch["arch"], version=_get_glibc_version() + ) + interpreter_info = { "result": "success", "markers": markers, @@ -554,7 +565,8 @@ def main() -> None: "stdlib": sysconfig.get_path("stdlib"), "scheme": get_scheme(), "virtualenv": get_virtualenv(), - "platform": get_operating_system_and_architecture(), + "platform": os_and_arch, + "manylinux_compatible": manylinux_compatible, # The `t` abiflag for freethreading Python. # https://peps.python.org/pep-0703/#build-configuration-changes "gil_disabled": bool(sysconfig.get_config_var("Py_GIL_DISABLED")), diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index f98b0040aa04..3e7378f351c1 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -33,6 +33,7 @@ pub struct Interpreter { markers: Box, scheme: Scheme, virtualenv: Scheme, + manylinux_compatible: bool, sys_prefix: PathBuf, sys_base_exec_prefix: PathBuf, sys_base_prefix: PathBuf, @@ -63,6 +64,7 @@ impl Interpreter { markers: Box::new(info.markers), scheme: info.scheme, virtualenv: info.virtualenv, + manylinux_compatible: info.manylinux_compatible, sys_prefix: info.sys_prefix, sys_base_exec_prefix: info.sys_base_exec_prefix, pointer_size: info.pointer_size, @@ -176,6 +178,7 @@ impl Interpreter { self.python_tuple(), self.implementation_name(), self.implementation_tuple(), + self.manylinux_compatible, self.gil_disabled, )?; self.tags.set(tags).expect("tags should not be set"); @@ -373,6 +376,11 @@ impl Interpreter { &self.virtualenv } + /// Return whether this interpreter is `manylinux` compatible. + pub fn manylinux_compatible(&self) -> bool { + self.manylinux_compatible + } + /// Return the [`PointerSize`] of the Python interpreter (i.e., 32- vs. 64-bit). pub fn pointer_size(&self) -> PointerSize { self.pointer_size @@ -555,6 +563,7 @@ struct InterpreterInfo { markers: MarkerEnvironment, scheme: Scheme, virtualenv: Scheme, + manylinux_compatible: bool, sys_prefix: PathBuf, sys_base_exec_prefix: PathBuf, sys_base_prefix: PathBuf, @@ -785,6 +794,7 @@ mod tests { }, "arch": "x86_64" }, + "manylinux_compatible": false, "markers": { "implementation_name": "cpython", "implementation_version": "3.12.0", diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 1e5627e0811c..b259994aac36 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -210,6 +210,7 @@ mod tests { }, "arch": "x86_64" }, + "manylinux_compatible": true, "markers": { "implementation_name": "{IMPLEMENTATION}", "implementation_version": "{FULL_VERSION}", diff --git a/crates/uv/src/commands/pip/mod.rs b/crates/uv/src/commands/pip/mod.rs index eba0c15cfb08..4821ad54583d 100644 --- a/crates/uv/src/commands/pip/mod.rs +++ b/crates/uv/src/commands/pip/mod.rs @@ -29,6 +29,7 @@ pub(crate) fn resolution_environment( (python_version.major(), python_version.minor()), interpreter.implementation_name(), interpreter.implementation_tuple(), + interpreter.manylinux_compatible(), interpreter.gil_disabled(), )?), (Some(python_platform), None) => Cow::Owned(Tags::from_env( @@ -36,6 +37,7 @@ pub(crate) fn resolution_environment( interpreter.python_tuple(), interpreter.implementation_name(), interpreter.implementation_tuple(), + interpreter.manylinux_compatible(), interpreter.gil_disabled(), )?), (None, Some(python_version)) => Cow::Owned(Tags::from_env( @@ -43,6 +45,7 @@ pub(crate) fn resolution_environment( (python_version.major(), python_version.minor()), interpreter.implementation_name(), interpreter.implementation_tuple(), + interpreter.manylinux_compatible(), interpreter.gil_disabled(), )?), (None, None) => Cow::Borrowed(interpreter.tags()?), diff --git a/docs/pip/compatibility.md b/docs/pip/compatibility.md index 5e77c35fff56..c1d6c1e2f05f 100644 --- a/docs/pip/compatibility.md +++ b/docs/pip/compatibility.md @@ -346,6 +346,34 @@ reuse any binary distributions that are already present in the local cache. Additionally, and in contrast to pip, uv's resolver will still read metadata from pre-built binary distributions when `--no-binary` is provided. +## `manylinux_compatible` enforcement + +[PEP 600](https://peps.python.org/pep-0600/#package-installers) describes a mechanism through which +Python distributors can opt out of `manylinux` compatibility by defining a `manylinux_compatible` +function on the `_manylinux` standard library module. + +uv respects `manylinux_compatible`, but only tests against the current glibc version, and applies +the return value of `manylinux_compatible` globally. + +In other words, if `manylinux_compatible` returns `True`, uv will treat the system as +`manylinux`-compatible; if it returns `False`, uv will treat the system as `manylinux`-incompatible, +without calling `manylinux_compatible` for every glibc version. + +This approach is not a complete implementation of the spec, but is compatible with common blanket +`manylinux_compatible` implementations like +[`no-manylinux`](https://pypi.org/project/no-manylinux/): + +```python +from __future__ import annotations +manylinux1_compatible = False +manylinux2010_compatible = False +manylinux2014_compatible = False + + +def manylinux_compatible(*_, **__): # PEP 600 + return False +``` + ## Bytecode compilation Unlike pip, uv does not compile `.py` files to `.pyc` files during installation by default (i.e., uv