Skip to content

Commit 5e38b44

Browse files
committed
Error when built wheel is for the wrong platform
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 Ready for review but I want to make some more testing before merging.
1 parent ab2f394 commit 5e38b44

File tree

8 files changed

+285
-79
lines changed

8 files changed

+285
-79
lines changed

crates/uv-bench/benches/uv.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ mod resolver {
131131
);
132132

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

137137
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,11 +6,12 @@ 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::Simplified;
1212
use uv_normalize::PackageName;
1313
use uv_pep440::{Version, VersionSpecifiers};
14+
use uv_platform_tags::Platform;
1415
use uv_pypi_types::{HashAlgorithm, HashDigest};
1516
use uv_redacted::DisplaySafeUrl;
1617
use uv_types::AnyErrorBuild;
@@ -74,6 +75,35 @@ pub enum Error {
7475
filename: Version,
7576
metadata: Version,
7677
},
78+
/// This shouldn't happen, it's a bug in the build backend.
79+
#[error(
80+
"The wheel `{}` built from source distribution is not compatible with the current Python {}.{} on {} {}",
81+
filename,
82+
python_version.0,
83+
python_version.1,
84+
python_platform.os(),
85+
python_platform.arch(),
86+
)]
87+
BuiltWheelIncompatibleHostPlatform {
88+
filename: WheelFilename,
89+
python_platform: Platform,
90+
python_version: (u8, u8),
91+
},
92+
/// This may happen when trying to cross-install native dependencies without their build backend
93+
/// being aware that the target is a cross-install.
94+
#[error(
95+
"The wheel `{}` built from source distribution is not compatible with the target Python {}.{} on {} {}. Consider using `--no-build` to disable building wheels.",
96+
filename,
97+
python_version.0,
98+
python_version.1,
99+
python_platform.os(),
100+
python_platform.arch(),
101+
)]
102+
BuiltWheelIncompatibleTargetPlatform {
103+
filename: WheelFilename,
104+
python_platform: Platform,
105+
python_version: (u8, u8),
106+
},
77107
#[error("Failed to parse metadata from built wheel")]
78108
Metadata(#[from] uv_pypi_types::MetadataError),
79109
#[error("Failed to read metadata: `{}`", _0.user_display())]

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
@@ -252,6 +252,7 @@ impl Interpreter {
252252
self.implementation_tuple(),
253253
self.manylinux_compatible,
254254
self.gil_disabled,
255+
false,
255256
)?;
256257
self.tags.set(tags).expect("tags should not be set");
257258
}

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

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

5454
use crate::commands::pip::loggers::DefaultResolveLogger;
55-
use crate::commands::pip::{operations, resolution_environment};
55+
use crate::commands::pip::{operations, resolution_markers, resolution_tags};
5656
use crate::commands::{ExitStatus, OutputWriter, diagnostics};
5757
use crate::printer::Printer;
5858

@@ -370,8 +370,16 @@ pub(crate) async fn pip_compile(
370370
ResolverEnvironment::universal(environments.into_markers()),
371371
)
372372
} else {
373-
let (tags, marker_env) =
374-
resolution_environment(python_version, python_platform, &interpreter)?;
373+
let tags = resolution_tags(
374+
python_version.as_ref(),
375+
python_platform.as_ref(),
376+
&interpreter,
377+
)?;
378+
let marker_env = resolution_markers(
379+
python_version.as_ref(),
380+
python_platform.as_ref(),
381+
&interpreter,
382+
);
375383
(Some(tags), ResolverEnvironment::specific(marker_env))
376384
};
377385

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
}

0 commit comments

Comments
 (0)