Further improve free-threading ABI incompatibility errors (#17491)

Expanding tag coverage from #17442
This commit is contained in:
Zanie Blue
2026-01-19 17:08:40 -06:00
committed by GitHub
parent 2faa1714d2
commit 0fae9d5610
6 changed files with 129 additions and 20 deletions
+52 -5
View File
@@ -18,7 +18,7 @@ use uv_distribution_types::{
};
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
use uv_platform_tags::{AbiTag, IncompatibleTag, TagCompatibility, Tags};
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::PythonEnvironment;
use uv_types::HashStrategy;
@@ -587,10 +587,25 @@ fn generate_wheel_compatibility_hint(filename: &WheelFilename, tags: &Tags) -> O
let wheel_abi = filename
.abi_tags()
.iter()
.map(|tag| format!("`{}`", tag.cyan()))
.map(|tag| match tag {
AbiTag::CPython {
gil_disabled: false,
python_version: (major, minor),
} => {
format!("the CPython {}.{} ABI (`{}`)", major, minor, tag.cyan())
}
AbiTag::Abi3 => format!("the stable ABI (`{}`)", tag.cyan()),
_ => {
if let Some(pretty) = tag.pretty() {
format!("the {} ABI (`{}`)", pretty.cyan(), tag.cyan())
} else {
format!("`{}`", tag.cyan())
}
}
})
.collect::<Vec<_>>()
.join(", ");
let message = if let Some(current) = tags.abi_tag() {
let current = if let Some(current) = tags.abi_tag() {
if let Some(pretty) = current.pretty() {
format!("{} (`{}`)", pretty.cyan(), current.cyan())
} else {
@@ -600,7 +615,7 @@ fn generate_wheel_compatibility_hint(filename: &WheelFilename, tags: &Tags) -> O
"free-threaded Python".to_string()
};
Some(format!(
"The wheel uses the stable ABI ({wheel_abi}), but you're using {message}, which is incompatible"
"You're using {current}, but the wheel was built for {wheel_abi}, which requires a GIL-enabled interpreter"
))
}
IncompatibleTag::Abi => {
@@ -809,7 +824,39 @@ mod tests {
let hint = generate_wheel_compatibility_hint(&filename, &tags).unwrap();
let hint = anstream::adapter::strip_str(&hint);
insta::assert_snapshot!(hint, @"The wheel uses the stable ABI (`abi3`), but you're using free-threaded CPython 3.14 (`cp314t`), which is incompatible");
insta::assert_snapshot!(hint, @"You're using free-threaded CPython 3.14 (`cp314t`), but the wheel was built for the stable ABI (`abi3`), which requires a GIL-enabled interpreter");
}
#[test]
fn test_gil_enabled_cpython_on_free_threaded_python_hint() {
// Create a Tags object for free-threaded Python 3.14
let platform = Platform::new(
Os::Manylinux {
major: 2,
minor: 28,
},
Arch::X86_64,
);
let tags = Tags::from_env(
&platform,
(3, 14), // python_version
"cpython", // implementation_name
(3, 14), // implementation_version
true, // manylinux_compatible
true, // gil_disabled (free-threaded)
false, // is_cross
)
.unwrap();
// Create a wheel filename with cp314 ABI tag (same version, GIL-enabled)
let filename =
WheelFilename::from_str("foo-1.0-cp314-cp314-manylinux_2_17_x86_64.whl").unwrap();
// Generate the hint
let hint = generate_wheel_compatibility_hint(&filename, &tags).unwrap();
let hint = anstream::adapter::strip_str(&hint);
insta::assert_snapshot!(hint, @"You're using free-threaded CPython 3.14 (`cp314t`), but the wheel was built for the CPython 3.14 ABI (`cp314`), which requires a GIL-enabled interpreter");
}
#[test]
+19 -4
View File
@@ -16,7 +16,7 @@ use uv_distribution_types::{
use uv_git_types::{GitLfs, GitOid};
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
use uv_platform_tags::{AbiTag, IncompatibleTag, TagCompatibility, Tags};
use uv_pypi_types::{DirInfo, DirectUrl, VcsInfo, VcsKind};
use crate::InstallationStrategy;
@@ -460,10 +460,25 @@ fn generate_dist_compatibility_hint(wheel_tags: &ExpandedTags, tags: &Tags) -> O
IncompatibleTag::FreethreadedAbi => {
let wheel_abi = wheel_tags
.abi_tags()
.map(|tag| format!("`{tag}`"))
.map(|tag| match tag {
AbiTag::CPython {
gil_disabled: false,
python_version: (major, minor),
} => {
format!("the CPython {major}.{minor} ABI (`{tag}`)")
}
AbiTag::Abi3 => format!("the stable ABI (`{tag}`)"),
_ => {
if let Some(pretty) = tag.pretty() {
format!("the {pretty} ABI (`{tag}`)")
} else {
format!("`{tag}`")
}
}
})
.collect::<Vec<_>>()
.join(", ");
let message = if let Some(current) = tags.abi_tag() {
let current = if let Some(current) = tags.abi_tag() {
if let Some(pretty) = current.pretty() {
format!("{pretty} (`{current}`)")
} else {
@@ -473,7 +488,7 @@ fn generate_dist_compatibility_hint(wheel_tags: &ExpandedTags, tags: &Tags) -> O
"free-threaded Python".to_string()
};
Some(format!(
"The distribution uses the stable ABI ({wheel_abi}), but you're using {message}, which is incompatible"
"You're using {current}, but the distribution was built for {wheel_abi}, which requires a GIL-enabled interpreter"
))
}
IncompatibleTag::Abi => {
+11 -3
View File
@@ -296,9 +296,17 @@ impl Tags {
wheel_abi_tags: &[AbiTag],
wheel_platform_tags: &[PlatformTag],
) -> TagCompatibility {
// The stable ABI (abi3) is not supported on free-threaded Python
if self.is_freethreaded && wheel_abi_tags.iter().all(|abi| *abi == AbiTag::Abi3) {
return TagCompatibility::Incompatible(IncompatibleTag::FreethreadedAbi);
// On free-threaded Python, check if any wheel ABI tag is compatible.
// Only `none` (pure Python) and free-threaded CPython ABIs (e.g., `cp313t`) are compatible.
if self.is_freethreaded {
let has_compatible_abi = wheel_abi_tags.iter().any(|abi| match abi {
AbiTag::None => true,
AbiTag::CPython { gil_disabled, .. } => *gil_disabled,
_ => false,
});
if !has_compatible_abi {
return TagCompatibility::Incompatible(IncompatibleTag::FreethreadedAbi);
}
}
let mut max_compatibility = TagCompatibility::Incompatible(IncompatibleTag::Invalid);
+47 -8
View File
@@ -13773,13 +13773,11 @@ fn record_uses_forward_slashes() -> Result<()> {
Ok(())
}
/// Test that abi3 wheels are rejected on free-threaded Python with a helpful error message.
/// Test ABI compatibility checking on free-threaded Python.
///
/// The stable ABI (abi3) is not supported by free-threaded Python, so we should provide
/// a clear error message when a user tries to install an abi3 wheel.
/// Free-threaded Python has a different ABI, so wheels must be built specifically for it.
#[test]
#[cfg(unix)]
fn abi3_wheel_on_freethreaded_python() {
fn abi_compatibility_on_freethreaded_python() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_managed_python_dirs()
@@ -13804,12 +13802,14 @@ fn abi3_wheel_on_freethreaded_python() {
.assert()
.success();
// Try to install an abi3 wheel - this should fail with a helpful error
// An abi3 wheel should fail with a helpful error
let wheel_path = context
.workspace_root
.join("test/links/abi3_package-1.0.0-cp37-abi3-manylinux_2_17_x86_64.whl");
uv_snapshot!(context.filters(), context.pip_install().arg(wheel_path), @r"
uv_snapshot!(context.filters(), context.pip_install()
.arg("--python-platform").arg("linux")
.arg(wheel_path), @r"
success: false
exit_code: 2
----- stdout -----
@@ -13819,6 +13819,45 @@ fn abi3_wheel_on_freethreaded_python() {
error: Failed to determine installation plan
Caused by: A path dependency is incompatible with the current platform: [WORKSPACE]/test/links/abi3_package-1.0.0-cp37-abi3-manylinux_2_17_x86_64.whl
hint: The wheel uses the stable ABI (`abi3`), but you're using free-threaded CPython 3.14 (`cp314t`), which is incompatible
hint: You're using free-threaded CPython 3.14 (`cp314t`), but the wheel was built for the stable ABI (`abi3`), which requires a GIL-enabled interpreter
");
// A GIL-enabled wheel for the same Python version should also fail
let wheel_path = context
.workspace_root
.join("test/links/cpython_package-1.0.0-cp314-cp314-manylinux_2_17_x86_64.whl");
uv_snapshot!(context.filters(), context.pip_install()
.arg("--python-platform").arg("linux")
.arg(wheel_path), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
error: Failed to determine installation plan
Caused by: A path dependency is incompatible with the current platform: [WORKSPACE]/test/links/cpython_package-1.0.0-cp314-cp314-manylinux_2_17_x86_64.whl
hint: You're using free-threaded CPython 3.14 (`cp314t`), but the wheel was built for the CPython 3.14 ABI (`cp314`), which requires a GIL-enabled interpreter
");
// A wheel with both cp314t (compatible) and abi3 (incompatible) should succeed
let wheel_path = context
.workspace_root
.join("test/links/multi_abi_package-1.0.0-cp314-cp314t.abi3-manylinux_2_17_x86_64.whl");
uv_snapshot!(context.filters(), context.pip_install()
.arg("--python-platform").arg("linux")
.arg(wheel_path), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ multi-abi-package==1.0.0 (from file://[WORKSPACE]/test/links/multi_abi_package-1.0.0-cp314-cp314t.abi3-manylinux_2_17_x86_64.whl)
");
}