From b73281d22238334fcd3fdc176c30b10e95e1d69a Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 5 Dec 2025 16:04:53 +0100 Subject: [PATCH] Error when built wheel is for the wrong platform (#16074) Error when a built wheel is for the wrong platform. This can happen especially when using `--python-platform` or `--python-version` with `uv pip install`. Fixes #16019 --- crates/uv-bench/benches/uv.rs | 2 +- .../src/distribution_database.rs | 21 +++ crates/uv-distribution/src/error.rs | 32 +++- crates/uv-distribution/src/source/mod.rs | 2 +- crates/uv-platform-tags/src/tags.rs | 35 +++- crates/uv-python/src/interpreter.rs | 1 + crates/uv/src/commands/pip/compile.rs | 14 +- crates/uv/src/commands/pip/mod.rs | 93 +++------- crates/uv/tests/it/common/mod.rs | 4 +- crates/uv/tests/it/pip_compile.rs | 147 ++++++++++++++++ crates/uv/tests/it/pip_install.rs | 164 +++++++++++++++++- 11 files changed, 433 insertions(+), 82 deletions(-) diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index cde282864..1e4e8046a 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -134,7 +134,7 @@ mod resolver { ); static TAGS: LazyLock = LazyLock::new(|| { - Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false).unwrap() + Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false, false).unwrap() }); pub(crate) async fn resolve( diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index d0415730e..ac2ecf4e4 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -385,6 +385,27 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .boxed_local() .await?; + // Check that the wheel is compatible with its install target. + // + // When building a build dependency for a cross-install, the build dependency needs + // to install and run on the host instead of the target. In this case the `tags` are already + // for the host instead of the target, so this check passes. + if !built_wheel.filename.is_compatible(tags) { + return if tags.is_cross() { + Err(Error::BuiltWheelIncompatibleTargetPlatform { + filename: built_wheel.filename, + python_platform: tags.python_platform().clone(), + python_version: tags.python_version(), + }) + } else { + Err(Error::BuiltWheelIncompatibleHostPlatform { + filename: built_wheel.filename, + python_platform: tags.python_platform().clone(), + python_version: tags.python_version(), + }) + }; + } + // Acquire the advisory lock. #[cfg(windows)] let _lock = { diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 8b6457483..172074dd0 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -6,12 +6,13 @@ use zip::result::ZipError; use crate::metadata::MetadataError; use uv_client::WrappedReqwestError; -use uv_distribution_filename::WheelFilenameError; +use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError}; use uv_fs::{LockedFileError, Simplified}; use uv_git::GitError; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; +use uv_platform_tags::Platform; use uv_pypi_types::{HashAlgorithm, HashDigest}; use uv_redacted::DisplaySafeUrl; use uv_types::AnyErrorBuild; @@ -77,6 +78,35 @@ pub enum Error { filename: Version, metadata: Version, }, + /// This shouldn't happen, it's a bug in the build backend. + #[error( + "The built wheel `{}` is not compatible with the current Python {}.{} on {} {}", + filename, + python_version.0, + python_version.1, + python_platform.os(), + python_platform.arch(), + )] + BuiltWheelIncompatibleHostPlatform { + filename: WheelFilename, + python_platform: Platform, + python_version: (u8, u8), + }, + /// This may happen when trying to cross-install native dependencies without their build backend + /// being aware that the target is a cross-install. + #[error( + "The built wheel `{}` is not compatible with the target Python {}.{} on {} {}. Consider using `--no-build` to disable building wheels.", + filename, + python_version.0, + python_version.1, + python_platform.os(), + python_platform.arch(), + )] + BuiltWheelIncompatibleTargetPlatform { + filename: WheelFilename, + python_platform: Platform, + python_version: (u8, u8), + }, #[error("Failed to parse metadata from built wheel")] Metadata(#[from] uv_pypi_types::MetadataError), #[error("Failed to read metadata: `{}`", _0.user_display())] diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 56c873ec2..6c4050019 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -2575,7 +2575,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - debug!("Finished building: {source}"); + debug!("Built `{source}` into `{disk_filename}`"); Ok((disk_filename, filename, metadata)) } diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 243c1e5bf..dd0993678 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -79,6 +79,13 @@ pub struct Tags { map: Arc>>>, /// The highest-priority tag for the Python version and platform. best: Option<(LanguageTag, AbiTag, PlatformTag)>, + /// Python platform used to generate the tags, for error messages. + python_platform: Platform, + /// Python version used to generate the tags, for error messages. + python_version: (u8, u8), + /// Whether the tags are for a different Python interpreter than the current one, for error + /// messages. + is_cross: bool, } impl Tags { @@ -86,7 +93,12 @@ impl Tags { /// /// Tags are prioritized based on their position in the given vector. Specifically, tags that /// appear earlier in the vector are given higher priority than tags that appear later. - pub fn new(tags: Vec<(LanguageTag, AbiTag, PlatformTag)>) -> Self { + fn new( + tags: Vec<(LanguageTag, AbiTag, PlatformTag)>, + python_platform: Platform, + python_version: (u8, u8), + is_cross: bool, + ) -> Self { // Store the highest-priority tag for each component. let best = tags.first().cloned(); @@ -104,6 +116,9 @@ impl Tags { Self { map: Arc::new(map), best, + python_platform, + python_version, + is_cross, } } @@ -116,6 +131,7 @@ impl Tags { implementation_version: (u8, u8), manylinux_compatible: bool, gil_disabled: bool, + is_cross: bool, ) -> Result { let implementation = Implementation::parse(implementation_name, gil_disabled)?; @@ -219,7 +235,7 @@ impl Tags { )); } } - Ok(Self::new(tags)) + Ok(Self::new(tags, platform.clone(), python_version, is_cross)) } /// Returns true when there exists at least one tag for this platform @@ -320,6 +336,18 @@ impl Tags { .map(|abis| abis.contains_key(&abi_tag)) .unwrap_or(false) } + + pub fn python_platform(&self) -> &Platform { + &self.python_platform + } + + pub fn python_version(&self) -> (u8, u8) { + self.python_version + } + + pub fn is_cross(&self) -> bool { + self.is_cross + } } /// The priority of a platform tag. @@ -1467,6 +1495,7 @@ mod tests { (3, 9), false, false, + false, ) .unwrap(); assert_snapshot!( @@ -1530,6 +1559,7 @@ mod tests { (3, 9), true, false, + false, ) .unwrap(); assert_snapshot!( @@ -2154,6 +2184,7 @@ mod tests { (3, 9), false, false, + false, ) .unwrap(); assert_snapshot!( diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 3586def80..bb65e9bf2 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -255,6 +255,7 @@ impl Interpreter { self.implementation_tuple(), self.manylinux_compatible, self.gil_disabled, + false, )?; self.tags.set(tags).expect("tags should not be set"); } diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 223f8c669..e8d86ea73 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -56,7 +56,7 @@ use uv_workspace::WorkspaceCache; use uv_workspace::pyproject::ExtraBuildDependencies; use crate::commands::pip::loggers::DefaultResolveLogger; -use crate::commands::pip::{operations, resolution_environment}; +use crate::commands::pip::{operations, resolution_markers, resolution_tags}; use crate::commands::{ExitStatus, OutputWriter, diagnostics}; use crate::printer::Printer; @@ -392,8 +392,16 @@ pub(crate) async fn pip_compile( ResolverEnvironment::universal(environments.into_markers()), ) } else { - let (tags, marker_env) = - resolution_environment(python_version, python_platform, &interpreter)?; + let tags = resolution_tags( + python_version.as_ref(), + python_platform.as_ref(), + &interpreter, + )?; + let marker_env = resolution_markers( + python_version.as_ref(), + python_platform.as_ref(), + &interpreter, + ); (Some(tags), ResolverEnvironment::specific(marker_env)) }; diff --git a/crates/uv/src/commands/pip/mod.rs b/crates/uv/src/commands/pip/mod.rs index 82b8836a4..5b23a073c 100644 --- a/crates/uv/src/commands/pip/mod.rs +++ b/crates/uv/src/commands/pip/mod.rs @@ -42,82 +42,33 @@ pub(crate) fn resolution_tags<'env>( python_platform: Option<&TargetTriple>, interpreter: &'env Interpreter, ) -> Result, TagsError> { - Ok(match (python_platform, python_version.as_ref()) { - (Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env( - &python_platform.platform(), - (python_version.major(), python_version.minor()), - interpreter.implementation_name(), - interpreter.implementation_tuple(), - python_platform.manylinux_compatible(), - interpreter.gil_disabled(), - )?), - (Some(python_platform), None) => Cow::Owned(Tags::from_env( - &python_platform.platform(), - interpreter.python_tuple(), - interpreter.implementation_name(), - interpreter.implementation_tuple(), - python_platform.manylinux_compatible(), - interpreter.gil_disabled(), - )?), - (None, Some(python_version)) => Cow::Owned(Tags::from_env( - interpreter.platform(), - (python_version.major(), python_version.minor()), - interpreter.implementation_name(), - interpreter.implementation_tuple(), - interpreter.manylinux_compatible(), - interpreter.gil_disabled(), - )?), - (None, None) => Cow::Borrowed(interpreter.tags()?), - }) -} + if python_platform.is_none() && python_version.is_none() { + return Ok(Cow::Borrowed(interpreter.tags()?)); + } -/// Determine the tags, markers, and interpreter to use for resolution. -pub(crate) fn resolution_environment( - python_version: Option, - python_platform: Option, - interpreter: &Interpreter, -) -> Result<(Cow<'_, Tags>, ResolverMarkerEnvironment), TagsError> { - let tags = match (python_platform, python_version.as_ref()) { - (Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env( + let (platform, manylinux_compatible) = if let Some(python_platform) = python_platform { + ( &python_platform.platform(), - (python_version.major(), python_version.minor()), - interpreter.implementation_name(), - interpreter.implementation_tuple(), python_platform.manylinux_compatible(), - interpreter.gil_disabled(), - )?), - (Some(python_platform), None) => Cow::Owned(Tags::from_env( - &python_platform.platform(), - interpreter.python_tuple(), - interpreter.implementation_name(), - interpreter.implementation_tuple(), - python_platform.manylinux_compatible(), - interpreter.gil_disabled(), - )?), - (None, Some(python_version)) => Cow::Owned(Tags::from_env( - interpreter.platform(), - (python_version.major(), python_version.minor()), - interpreter.implementation_name(), - interpreter.implementation_tuple(), - interpreter.manylinux_compatible(), - interpreter.gil_disabled(), - )?), - (None, None) => Cow::Borrowed(interpreter.tags()?), + ) + } else { + (interpreter.platform(), interpreter.manylinux_compatible()) }; - // Apply the platform tags to the markers. - let markers = match (python_platform, python_version) { - (Some(python_platform), Some(python_version)) => ResolverMarkerEnvironment::from( - python_version.markers(&python_platform.markers(interpreter.markers())), - ), - (Some(python_platform), None) => { - ResolverMarkerEnvironment::from(python_platform.markers(interpreter.markers())) - } - (None, Some(python_version)) => { - ResolverMarkerEnvironment::from(python_version.markers(interpreter.markers())) - } - (None, None) => interpreter.resolver_marker_environment(), + let version_tuple = if let Some(python_version) = python_version { + (python_version.major(), python_version.minor()) + } else { + interpreter.python_tuple() }; - Ok((tags, markers)) + let tags = Tags::from_env( + platform, + version_tuple, + interpreter.implementation_name(), + interpreter.implementation_tuple(), + manylinux_compatible, + interpreter.gil_disabled(), + true, + )?; + Ok(Cow::Owned(tags)) } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4fd4a7811..045ea1d32 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -561,8 +561,8 @@ impl TestContext { } /// Add a custom filter to the `TestContext`. - pub fn with_filter(mut self, filter: (String, String)) -> Self { - self.filters.push(filter); + pub fn with_filter(mut self, filter: (impl Into, impl Into)) -> Self { + self.filters.push((filter.0.into(), filter.1.into())); self } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 169850fb2..4f912558e 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -17921,3 +17921,150 @@ fn post_release_less_than() -> Result<()> { Ok(()) } + +/// When using `uv pip compile --python-platform`, it should not matter what platform the built +/// wheel is for. We want to ensure that uv accepts both wheels for the host platform and wheels for +/// the target platform in `uv pip compile`. This is unlike `uv pip install --python-platform`, +/// where a wheel for the target platform is required. +/// +/// The main test (first snapshot) builds a Windows wheel, and targets macOS. If we run this test on +/// Linux, the wheel tag is neither host nor target, but we don't have a reason to reject the +/// Windows tag either. +#[test] +fn compile_with_python_platform_and_built_wheel_for_different_platform() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("./project")?; + + let project = context.temp_dir.child("project"); + // This pyproject.toml uses dynamic versioning to prevent uv from reading static metadata. + project.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "project" + requires-python = ">=3.12" + dependencies = [ + "sniffio; sys_platform == 'linux'", + "tqdm; sys_platform == 'darwin'" + ] + dynamic = ["version"] + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build" + + [tool.hatch.version] + path = "src/project/__init__.py" + "#})?; + // Rename the built wheel to a Windows wheel + project.child("build.py").write_str(indoc! {r#" + import os + + import hatchling.build + + __all__ = ["build_sdist", "build_wheel"] + + + def build_sdist( + sdist_directory: str, config_settings: "Mapping[Any, Any] | None" = None + ) -> str: + hatchling.build.build_sdist(sdist_directory, config_settings) + + + def build_wheel( + wheel_directory: str, + config_settings: "Mapping[Any, Any] | None" = None, + metadata_directory: "str | None" = None, + ) -> str: + name = hatchling.build.build_wheel( + wheel_directory, config_settings, metadata_directory + ) + # Don't do this at home, ask your build backend instead so it also changes the + # `WHEEL` file. + new_name = "project-0.1.0-cp312-abi3-win_amd64.whl" + os.rename( + os.path.join(wheel_directory, name), os.path.join(wheel_directory, new_name) + ) + return new_name + "#})?; + project + .child("src/project/__init__.py") + .write_str(r#"__version__ = "0.1.0"\n"#)?; + + uv_snapshot!(context + .pip_compile() + .arg("requirements.in") + .arg("--python-platform") + .arg("macos"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python-platform macos + ./project + # via -r requirements.in + tqdm==4.66.2 + # via project + + ----- stderr ----- + Resolved 2 packages in [TIME] + "); + + uv_snapshot!(context + .pip_compile() + .arg("requirements.in") + .arg("--python-platform") + .arg("linux"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python-platform linux + ./project + # via -r requirements.in + sniffio==1.3.1 + # via project + + ----- stderr ----- + Resolved 2 packages in [TIME] + "); + + uv_snapshot!(context + .pip_compile() + .arg("requirements.in") + .arg("--python-platform") + .arg("windows"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python-platform windows + ./project + # via -r requirements.in + + ----- stderr ----- + Resolved 1 package in [TIME] + "); + + uv_snapshot!(context + .pip_compile() + .arg("requirements.in") + .arg("--universal"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal + ./project + # via -r requirements.in + sniffio==1.3.1 ; sys_platform == 'linux' + # via project + tqdm==4.66.2 ; sys_platform == 'darwin' + # via project + + ----- stderr ----- + Resolved 3 packages in [TIME] + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index a1711c96f..a99466341 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7,7 +7,7 @@ use assert_fs::prelude::*; use flate2::write::GzEncoder; use fs_err as fs; use fs_err::File; -use indoc::indoc; +use indoc::{formatdoc, indoc}; use predicates::prelude::predicate; use url::Url; use wiremock::{ @@ -13300,3 +13300,165 @@ fn install_missing_python_version_with_target() { "### ); } + +/// Use a wheel that is only compatible with Python 3.13 with Python 3.12 or Python 3.13 to simulate +/// a wheel build for the wrong platform in a cross-install scenario. Ensure that we catch this case +/// and error accordingly. Additionally, we ensure that for a build dependency, which builds and +/// runs on the host, not the target, we accept wheel platforms for the host. +#[test] +fn build_backend_wrong_wheel_platform() -> Result<()> { + let context = TestContext::new_with_versions(&["3.12", "3.13"]) + .with_filter((r" on [^ ]+ [^ ]+\.", " on [ARCH] [OS].")) + .with_filter((r" on [^ ]+ [^ ]+$", " on [ARCH] [OS]")); + + let py313 = context.temp_dir.child("child"); + py313.create_dir_all()?; + let py313_pyproject_toml = py313.child("pyproject.toml"); + py313_pyproject_toml.write_str(indoc! {r#" + [project] + name = "py313" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + + let build_backend = py313.child("build_backend.py"); + build_backend.write_str(indoc! {r#" + import os + + from hatchling.build import * + from hatchling.build import build_wheel as build_wheel_original + + + def build_wheel( + wheel_directory: str, + config_settings: "Mapping[Any, Any] | None" = None, + metadata_directory: "str | None" = None, + ) -> str: + filename = build_wheel_original( + wheel_directory, config_settings, metadata_directory + ) + py313_wheel = "py313-0.1.0-py313-none-any.whl" + os.rename( + os.path.join(wheel_directory, filename), + os.path.join(wheel_directory, py313_wheel), + ) + return py313_wheel + "#})?; + py313.child("src/py313/__init__.py").touch()?; + + // Test the matrix of + // (compatible host, incompatible host) x (compatible target, incompatible target) + + // A Python 3.13 host with a 3.13 implicit target works. + context.venv().arg("-p").arg("3.13").assert().success(); + uv_snapshot!(context.filters(), context.pip_install().arg("./child"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + py313==0.1.0 (from file://[TEMP_DIR]/child) + "); + + // A Python 3.13 host with a 3.12 explicit target fails. + context.venv().arg("-p").arg("3.13").assert().success(); + uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("./child"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + × Failed to build `py313 @ file://[TEMP_DIR]/child` + ╰─▶ The built wheel `py313-0.1.0-py313-none-any.whl` is not compatible with the target Python 3.12 on [ARCH] [OS]. Consider using `--no-build` to disable building wheels. + "); + + // A python 3.12 host with a 3.13 explicit target works. + context.venv().arg("-p").arg("3.13").assert().success(); + uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.13").arg("./child"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + ~ py313==0.1.0 (from file://[TEMP_DIR]/child) + "); + + // A Python 3.13 host with a 3.12 explicit target fails. + context.venv().arg("-p").arg("3.13").assert().success(); + uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("./child"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + × Failed to build `py313 @ file://[TEMP_DIR]/child` + ╰─▶ The built wheel `py313-0.1.0-py313-none-any.whl` is not compatible with the target Python 3.12 on [ARCH] [OS]. Consider using `--no-build` to disable building wheels. + "); + + // Create a project that will resolve to a non-latest version of `anyio` + let parent = &context.temp_dir; + let pyproject_toml = parent.child("pyproject.toml"); + pyproject_toml.write_str(&formatdoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["hatchling", "py313 @ file://{py313}"] + build-backend = "hatchling.build" + "#, + py313 = py313.path().portable_display() + })?; + context + .temp_dir + .child("src") + .child("parent") + .child("__init__.py") + .touch()?; + + // A build host of 3.13 works. + context.venv().arg("-p").arg("3.13").assert().success(); + uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("."), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + parent==0.1.0 (from file://[TEMP_DIR]/) + "); + + // A build host of 3.12 fails. + context.venv().arg("-p").arg("3.12").assert().success(); + uv_snapshot!(context.filters(), context.pip_install().arg("--python-version").arg("3.12").arg("."), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + × Failed to build `parent @ file://[TEMP_DIR]/` + ├─▶ Failed to install requirements from `build-system.requires` + ├─▶ Failed to build `py313 @ file://[TEMP_DIR]/child` + ╰─▶ The built wheel `py313-0.1.0-py313-none-any.whl` is not compatible with the current Python 3.12 on [ARCH] [OS] + "); + + Ok(()) +}