Skip to content

Commit b73281d

Browse files
authored
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
1 parent 9f58280 commit b73281d

11 files changed

Lines changed: 433 additions & 82 deletions

File tree

crates/uv-bench/benches/uv.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ mod resolver {
134134
);
135135

136136
static TAGS: LazyLock<Tags> = LazyLock::new(|| {
137-
Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false).unwrap()
137+
Tags::from_env(&PLATFORM, (3, 11), "cpython", (3, 11), false, false, false).unwrap()
138138
});
139139

140140
pub(crate) async fn resolve(

crates/uv-distribution/src/distribution_database.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,27 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
385385
.boxed_local()
386386
.await?;
387387

388+
// Check that the wheel is compatible with its install target.
389+
//
390+
// When building a build dependency for a cross-install, the build dependency needs
391+
// to install and run on the host instead of the target. In this case the `tags` are already
392+
// for the host instead of the target, so this check passes.
393+
if !built_wheel.filename.is_compatible(tags) {
394+
return if tags.is_cross() {
395+
Err(Error::BuiltWheelIncompatibleTargetPlatform {
396+
filename: built_wheel.filename,
397+
python_platform: tags.python_platform().clone(),
398+
python_version: tags.python_version(),
399+
})
400+
} else {
401+
Err(Error::BuiltWheelIncompatibleHostPlatform {
402+
filename: built_wheel.filename,
403+
python_platform: tags.python_platform().clone(),
404+
python_version: tags.python_version(),
405+
})
406+
};
407+
}
408+
388409
// Acquire the advisory lock.
389410
#[cfg(windows)]
390411
let _lock = {

crates/uv-distribution/src/error.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use zip::result::ZipError;
66

77
use crate::metadata::MetadataError;
88
use uv_client::WrappedReqwestError;
9-
use uv_distribution_filename::WheelFilenameError;
9+
use uv_distribution_filename::{WheelFilename, WheelFilenameError};
1010
use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError};
1111
use uv_fs::{LockedFileError, Simplified};
1212
use uv_git::GitError;
1313
use uv_normalize::PackageName;
1414
use uv_pep440::{Version, VersionSpecifiers};
15+
use uv_platform_tags::Platform;
1516
use uv_pypi_types::{HashAlgorithm, HashDigest};
1617
use uv_redacted::DisplaySafeUrl;
1718
use uv_types::AnyErrorBuild;
@@ -77,6 +78,35 @@ pub enum Error {
7778
filename: Version,
7879
metadata: Version,
7980
},
81+
/// This shouldn't happen, it's a bug in the build backend.
82+
#[error(
83+
"The built wheel `{}` is not compatible with the current Python {}.{} on {} {}",
84+
filename,
85+
python_version.0,
86+
python_version.1,
87+
python_platform.os(),
88+
python_platform.arch(),
89+
)]
90+
BuiltWheelIncompatibleHostPlatform {
91+
filename: WheelFilename,
92+
python_platform: Platform,
93+
python_version: (u8, u8),
94+
},
95+
/// This may happen when trying to cross-install native dependencies without their build backend
96+
/// being aware that the target is a cross-install.
97+
#[error(
98+
"The built wheel `{}` is not compatible with the target Python {}.{} on {} {}. Consider using `--no-build` to disable building wheels.",
99+
filename,
100+
python_version.0,
101+
python_version.1,
102+
python_platform.os(),
103+
python_platform.arch(),
104+
)]
105+
BuiltWheelIncompatibleTargetPlatform {
106+
filename: WheelFilename,
107+
python_platform: Platform,
108+
python_version: (u8, u8),
109+
},
80110
#[error("Failed to parse metadata from built wheel")]
81111
Metadata(#[from] uv_pypi_types::MetadataError),
82112
#[error("Failed to read metadata: `{}`", _0.user_display())]

crates/uv-distribution/src/source/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2575,7 +2575,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
25752575
.await
25762576
.map_err(Error::CacheWrite)?;
25772577

2578-
debug!("Finished building: {source}");
2578+
debug!("Built `{source}` into `{disk_filename}`");
25792579
Ok((disk_filename, filename, metadata))
25802580
}
25812581

crates/uv-platform-tags/src/tags.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,26 @@ pub struct Tags {
7979
map: Arc<FxHashMap<LanguageTag, FxHashMap<AbiTag, FxHashMap<PlatformTag, TagPriority>>>>,
8080
/// The highest-priority tag for the Python version and platform.
8181
best: Option<(LanguageTag, AbiTag, PlatformTag)>,
82+
/// Python platform used to generate the tags, for error messages.
83+
python_platform: Platform,
84+
/// Python version used to generate the tags, for error messages.
85+
python_version: (u8, u8),
86+
/// Whether the tags are for a different Python interpreter than the current one, for error
87+
/// messages.
88+
is_cross: bool,
8289
}
8390

8491
impl Tags {
8592
/// Create a new set of tags.
8693
///
8794
/// Tags are prioritized based on their position in the given vector. Specifically, tags that
8895
/// appear earlier in the vector are given higher priority than tags that appear later.
89-
pub fn new(tags: Vec<(LanguageTag, AbiTag, PlatformTag)>) -> Self {
96+
fn new(
97+
tags: Vec<(LanguageTag, AbiTag, PlatformTag)>,
98+
python_platform: Platform,
99+
python_version: (u8, u8),
100+
is_cross: bool,
101+
) -> Self {
90102
// Store the highest-priority tag for each component.
91103
let best = tags.first().cloned();
92104

@@ -104,6 +116,9 @@ impl Tags {
104116
Self {
105117
map: Arc::new(map),
106118
best,
119+
python_platform,
120+
python_version,
121+
is_cross,
107122
}
108123
}
109124

@@ -116,6 +131,7 @@ impl Tags {
116131
implementation_version: (u8, u8),
117132
manylinux_compatible: bool,
118133
gil_disabled: bool,
134+
is_cross: bool,
119135
) -> Result<Self, TagsError> {
120136
let implementation = Implementation::parse(implementation_name, gil_disabled)?;
121137

@@ -219,7 +235,7 @@ impl Tags {
219235
));
220236
}
221237
}
222-
Ok(Self::new(tags))
238+
Ok(Self::new(tags, platform.clone(), python_version, is_cross))
223239
}
224240

225241
/// Returns true when there exists at least one tag for this platform
@@ -320,6 +336,18 @@ impl Tags {
320336
.map(|abis| abis.contains_key(&abi_tag))
321337
.unwrap_or(false)
322338
}
339+
340+
pub fn python_platform(&self) -> &Platform {
341+
&self.python_platform
342+
}
343+
344+
pub fn python_version(&self) -> (u8, u8) {
345+
self.python_version
346+
}
347+
348+
pub fn is_cross(&self) -> bool {
349+
self.is_cross
350+
}
323351
}
324352

325353
/// The priority of a platform tag.
@@ -1467,6 +1495,7 @@ mod tests {
14671495
(3, 9),
14681496
false,
14691497
false,
1498+
false,
14701499
)
14711500
.unwrap();
14721501
assert_snapshot!(
@@ -1530,6 +1559,7 @@ mod tests {
15301559
(3, 9),
15311560
true,
15321561
false,
1562+
false,
15331563
)
15341564
.unwrap();
15351565
assert_snapshot!(
@@ -2154,6 +2184,7 @@ mod tests {
21542184
(3, 9),
21552185
false,
21562186
false,
2187+
false,
21572188
)
21582189
.unwrap();
21592190
assert_snapshot!(

crates/uv-python/src/interpreter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ impl Interpreter {
255255
self.implementation_tuple(),
256256
self.manylinux_compatible,
257257
self.gil_disabled,
258+
false,
258259
)?;
259260
self.tags.set(tags).expect("tags should not be set");
260261
}

crates/uv/src/commands/pip/compile.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ use uv_workspace::WorkspaceCache;
5656
use uv_workspace::pyproject::ExtraBuildDependencies;
5757

5858
use crate::commands::pip::loggers::DefaultResolveLogger;
59-
use crate::commands::pip::{operations, resolution_environment};
59+
use crate::commands::pip::{operations, resolution_markers, resolution_tags};
6060
use crate::commands::{ExitStatus, OutputWriter, diagnostics};
6161
use crate::printer::Printer;
6262

@@ -392,8 +392,16 @@ pub(crate) async fn pip_compile(
392392
ResolverEnvironment::universal(environments.into_markers()),
393393
)
394394
} else {
395-
let (tags, marker_env) =
396-
resolution_environment(python_version, python_platform, &interpreter)?;
395+
let tags = resolution_tags(
396+
python_version.as_ref(),
397+
python_platform.as_ref(),
398+
&interpreter,
399+
)?;
400+
let marker_env = resolution_markers(
401+
python_version.as_ref(),
402+
python_platform.as_ref(),
403+
&interpreter,
404+
);
397405
(Some(tags), ResolverEnvironment::specific(marker_env))
398406
};
399407

crates/uv/src/commands/pip/mod.rs

Lines changed: 22 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -42,82 +42,33 @@ pub(crate) fn resolution_tags<'env>(
4242
python_platform: Option<&TargetTriple>,
4343
interpreter: &'env Interpreter,
4444
) -> Result<Cow<'env, Tags>, TagsError> {
45-
Ok(match (python_platform, python_version.as_ref()) {
46-
(Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env(
47-
&python_platform.platform(),
48-
(python_version.major(), python_version.minor()),
49-
interpreter.implementation_name(),
50-
interpreter.implementation_tuple(),
51-
python_platform.manylinux_compatible(),
52-
interpreter.gil_disabled(),
53-
)?),
54-
(Some(python_platform), None) => Cow::Owned(Tags::from_env(
55-
&python_platform.platform(),
56-
interpreter.python_tuple(),
57-
interpreter.implementation_name(),
58-
interpreter.implementation_tuple(),
59-
python_platform.manylinux_compatible(),
60-
interpreter.gil_disabled(),
61-
)?),
62-
(None, Some(python_version)) => Cow::Owned(Tags::from_env(
63-
interpreter.platform(),
64-
(python_version.major(), python_version.minor()),
65-
interpreter.implementation_name(),
66-
interpreter.implementation_tuple(),
67-
interpreter.manylinux_compatible(),
68-
interpreter.gil_disabled(),
69-
)?),
70-
(None, None) => Cow::Borrowed(interpreter.tags()?),
71-
})
72-
}
45+
if python_platform.is_none() && python_version.is_none() {
46+
return Ok(Cow::Borrowed(interpreter.tags()?));
47+
}
7348

74-
/// Determine the tags, markers, and interpreter to use for resolution.
75-
pub(crate) fn resolution_environment(
76-
python_version: Option<PythonVersion>,
77-
python_platform: Option<TargetTriple>,
78-
interpreter: &Interpreter,
79-
) -> Result<(Cow<'_, Tags>, ResolverMarkerEnvironment), TagsError> {
80-
let tags = match (python_platform, python_version.as_ref()) {
81-
(Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env(
49+
let (platform, manylinux_compatible) = if let Some(python_platform) = python_platform {
50+
(
8251
&python_platform.platform(),
83-
(python_version.major(), python_version.minor()),
84-
interpreter.implementation_name(),
85-
interpreter.implementation_tuple(),
8652
python_platform.manylinux_compatible(),
87-
interpreter.gil_disabled(),
88-
)?),
89-
(Some(python_platform), None) => Cow::Owned(Tags::from_env(
90-
&python_platform.platform(),
91-
interpreter.python_tuple(),
92-
interpreter.implementation_name(),
93-
interpreter.implementation_tuple(),
94-
python_platform.manylinux_compatible(),
95-
interpreter.gil_disabled(),
96-
)?),
97-
(None, Some(python_version)) => Cow::Owned(Tags::from_env(
98-
interpreter.platform(),
99-
(python_version.major(), python_version.minor()),
100-
interpreter.implementation_name(),
101-
interpreter.implementation_tuple(),
102-
interpreter.manylinux_compatible(),
103-
interpreter.gil_disabled(),
104-
)?),
105-
(None, None) => Cow::Borrowed(interpreter.tags()?),
53+
)
54+
} else {
55+
(interpreter.platform(), interpreter.manylinux_compatible())
10656
};
10757

108-
// Apply the platform tags to the markers.
109-
let markers = match (python_platform, python_version) {
110-
(Some(python_platform), Some(python_version)) => ResolverMarkerEnvironment::from(
111-
python_version.markers(&python_platform.markers(interpreter.markers())),
112-
),
113-
(Some(python_platform), None) => {
114-
ResolverMarkerEnvironment::from(python_platform.markers(interpreter.markers()))
115-
}
116-
(None, Some(python_version)) => {
117-
ResolverMarkerEnvironment::from(python_version.markers(interpreter.markers()))
118-
}
119-
(None, None) => interpreter.resolver_marker_environment(),
58+
let version_tuple = if let Some(python_version) = python_version {
59+
(python_version.major(), python_version.minor())
60+
} else {
61+
interpreter.python_tuple()
12062
};
12163

122-
Ok((tags, markers))
64+
let tags = Tags::from_env(
65+
platform,
66+
version_tuple,
67+
interpreter.implementation_name(),
68+
interpreter.implementation_tuple(),
69+
manylinux_compatible,
70+
interpreter.gil_disabled(),
71+
true,
72+
)?;
73+
Ok(Cow::Owned(tags))
12374
}

crates/uv/tests/it/common/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,8 @@ impl TestContext {
561561
}
562562

563563
/// Add a custom filter to the `TestContext`.
564-
pub fn with_filter(mut self, filter: (String, String)) -> Self {
565-
self.filters.push(filter);
564+
pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
565+
self.filters.push((filter.0.into(), filter.1.into()));
566566
self
567567
}
568568

0 commit comments

Comments
 (0)