1use std::fmt;
2use std::path::PathBuf;
3
4use owo_colors::OwoColorize;
5use tokio::task::JoinError;
6
7use crate::metadata::MetadataError;
8use uv_cache::Error as CacheError;
9use uv_client::WrappedReqwestError;
10use uv_distribution_filename::{VariantLabel, WheelFilename, WheelFilenameError};
11use uv_distribution_types::{
12 InstalledDist, InstalledDistError, IsBuildBackendError, UnsupportedVariantProperties,
13};
14use uv_fs::Simplified;
15use uv_git::GitError;
16use uv_macros::DebugNoInline;
17use uv_normalize::PackageName;
18use uv_pep440::{Version, VersionSpecifiers};
19use uv_platform_tags::Platform;
20use uv_pypi_types::{HashAlgorithm, HashDigest};
21use uv_python_types::PythonVariant;
22use uv_redacted::SafeUrl;
23use uv_types::AnyErrorBuild;
24
25#[derive(Debug, Clone, Copy)]
26pub struct PythonVersion {
27 pub(crate) version: (u8, u8),
28 pub(crate) variant: PythonVariant,
29}
30
31impl fmt::Display for PythonVersion {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 let (major, minor) = self.version;
34 write!(f, "{major}.{minor}{}", self.variant.executable_suffix())
35 }
36}
37
38#[derive(DebugNoInline, thiserror::Error)]
39pub enum Error {
40 #[error("Wheel variants are not supported yet: `{0}`")]
41 UnsupportedWheelVariant(Box<WheelFilename>),
42 #[error("Wheel variant `{label}` requires unsupported properties: {properties}")]
43 IncompatibleWheelVariant {
44 label: VariantLabel,
45 properties: UnsupportedVariantProperties,
46 },
47 #[error("Building source distributions is disabled")]
48 NoBuild,
49 #[error("Building source distributions for `{0}` is disabled")]
50 NoBuildPackage(PackageName),
51
52 #[error(transparent)]
54 InvalidUrl(#[from] uv_distribution_types::ToUrlError),
55 #[error("Expected a file URL, but received: {0}")]
56 NonFileUrl(SafeUrl),
57 #[error(transparent)]
58 Git(#[from] uv_git::GitResolverError),
59 #[error(transparent)]
60 Reqwest(#[from] WrappedReqwestError),
61 #[error(transparent)]
62 Client(#[from] uv_client::Error),
63 #[error(transparent)]
64 ClientBuild(#[from] uv_client::ClientBuildError),
65
66 #[error("Failed to read from the distribution cache")]
68 CacheRead(#[source] std::io::Error),
69 #[error("Failed to write to the distribution cache")]
70 CacheWrite(#[source] std::io::Error),
71 #[error("Failed to acquire lock on the distribution cache")]
72 CacheLock(#[source] CacheError),
73 #[error("Failed to deserialize cache entry")]
74 CacheDecode(#[from] rmp_serde::decode::Error),
75 #[error("Failed to serialize cache entry")]
76 CacheEncode(#[from] rmp_serde::encode::Error),
77 #[error("Failed to walk the distribution cache")]
78 CacheWalk(#[source] walkdir::Error),
79 #[error(transparent)]
80 CacheInfo(#[from] uv_cache_info::CacheInfoError),
81
82 #[error(transparent)]
84 Build(AnyErrorBuild),
85 #[error("Built wheel has an invalid filename")]
86 WheelFilename(#[from] WheelFilenameError),
87 #[error("Package metadata name `{metadata}` does not match given name `{given}`")]
88 WheelMetadataNameMismatch {
89 given: PackageName,
90 metadata: PackageName,
91 },
92 #[error("Package metadata version `{metadata}` does not match given version `{given}`")]
93 WheelMetadataVersionMismatch { given: Version, metadata: Version },
94 #[error(
95 "Package metadata name `{metadata}` does not match `{filename}` from the wheel filename"
96 )]
97 WheelFilenameNameMismatch {
98 filename: PackageName,
99 metadata: PackageName,
100 },
101 #[error(
102 "Package metadata version `{metadata}` does not match `{filename}` from the wheel filename"
103 )]
104 WheelFilenameVersionMismatch {
105 filename: Version,
106 metadata: Version,
107 },
108 #[error(
110 "The built wheel `{}` is not compatible with the current Python {} on {}",
111 filename,
112 python_version,
113 python_platform.pretty(),
114 )]
115 BuiltWheelIncompatibleHostPlatform {
116 filename: WheelFilename,
117 python_platform: Platform,
118 python_version: PythonVersion,
119 },
120 #[error(
123 "The built wheel `{}` is not compatible with the target Python {} on {}. Consider using `--no-build` to disable building wheels.",
124 filename,
125 python_version,
126 python_platform.pretty(),
127 )]
128 BuiltWheelIncompatibleTargetPlatform {
129 filename: WheelFilename,
130 python_platform: Platform,
131 python_version: PythonVersion,
132 },
133 #[error("Failed to parse metadata from built wheel")]
134 Metadata(#[from] uv_pypi_types::MetadataError),
135 #[error("Failed to read metadata: {}", _0.user_display())]
136 WheelMetadata(PathBuf, #[source] Box<uv_metadata::Error>),
137 #[error("Failed to read metadata from installed package `{0}`")]
138 ReadInstalled(Box<InstalledDist>, #[source] InstalledDistError),
139 #[error("Failed to extract archive: {0}")]
140 Extract(String, #[source] uv_extract::Error),
141 #[error("The source distribution is missing a `PKG-INFO` file")]
142 MissingPkgInfo,
143 #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())]
144 MissingSubdirectory(SafeUrl, PathBuf),
145 #[error("The source distribution `{0}` is missing Git LFS artifacts.")]
146 MissingSourceDistGitLfsArtifacts(SafeUrl, #[source] GitError),
147 #[error("The wheel `{0}` is missing Git LFS artifacts.")]
148 MissingWheelGitLfsArtifacts(SafeUrl, #[source] GitError),
149 #[error("Failed to extract static metadata from `PKG-INFO`")]
150 PkgInfo(#[source] uv_pypi_types::MetadataError),
151 #[error("The source distribution is missing a `pyproject.toml` file")]
152 MissingPyprojectToml,
153 #[error("Failed to extract static metadata from `pyproject.toml`")]
154 PyprojectToml(#[source] uv_pypi_types::MetadataError),
155 #[error(transparent)]
156 MetadataLowering(#[from] MetadataError),
157 #[error("Distribution not found at: {0}")]
158 NotFound(SafeUrl),
159 #[error("Attempted to re-extract the source distribution for `{}`, but the {} hash didn't match. Run `{}` to clear the cache.", _0, _1, "uv cache clean".green())]
160 CacheHeal(String, HashAlgorithm),
161 #[error("The source distribution requires Python {0}, but {1} is installed")]
162 RequiresPython(VersionSpecifiers, Version),
163 #[error("Failed to identify base Python interpreter")]
164 BaseInterpreter(#[source] std::io::Error),
165
166 #[error(transparent)]
169 ReqwestMiddlewareError(#[from] anyhow::Error),
170
171 #[error("The task executor is broken, did some other task panic?")]
173 Join(#[from] JoinError),
174
175 #[error("Failed to hash distribution")]
177 HashExhaustion(#[source] std::io::Error),
178
179 #[error("Hash mismatch for `{distribution}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")]
180 MismatchedHashes {
181 distribution: String,
182 expected: String,
183 actual: String,
184 },
185
186 #[error(
187 "Size mismatch for `{distribution}`: expected {expected} bytes, but downloaded {actual} bytes"
188 )]
189 MismatchedSize {
190 distribution: String,
191 expected: u64,
192 actual: u64,
193 },
194
195 #[error(
196 "Content-Length mismatch for `{distribution}`: expected {expected} bytes, but the server advertised {actual} bytes"
197 )]
198 MismatchedContentLength {
199 distribution: String,
200 expected: u64,
201 actual: u64,
202 },
203
204 #[error(
205 "Range response size mismatch for `{distribution}`: expected {expected} bytes from Content-Range, but received {actual} bytes"
206 )]
207 MismatchedRangeSize {
208 distribution: String,
209 expected: u64,
210 actual: u64,
211 },
212
213 #[error(
214 "Hash-checking is enabled, but no hashes were provided or computed for: {distribution}"
215 )]
216 MissingHashes { distribution: String },
217
218 #[error(
219 "Hash-checking is enabled, but no hashes were computed for `{distribution}`\n\nExpected:\n{expected}"
220 )]
221 MissingActualHashes {
222 distribution: String,
223 expected: String,
224 },
225
226 #[error(
227 "Hash-checking is enabled, but no hashes were provided for `{distribution}`\n\nComputed:\n{actual}"
228 )]
229 MissingExpectedHashes {
230 distribution: String,
231 actual: String,
232 },
233
234 #[error("Hash-checking is not supported for local directories: {0}")]
235 HashesNotSupportedSourceTree(String),
236
237 #[error("Hash-checking is not supported for Git repositories: {0}")]
238 HashesNotSupportedGit(String),
239
240 #[error(transparent)]
241 InstallWheelError(uv_install_wheel::Error),
242}
243
244fn is_not_found_error(mut error: &(dyn std::error::Error + 'static)) -> bool {
245 loop {
246 if error
247 .downcast_ref::<reqwest::Error>()
248 .and_then(reqwest::Error::status)
249 .is_some_and(|status| status == reqwest::StatusCode::NOT_FOUND)
250 {
251 return true;
252 }
253 let Some(source) = error.source() else {
254 return false;
255 };
256 error = source;
257 }
258}
259
260impl From<reqwest::Error> for Error {
261 fn from(error: reqwest::Error) -> Self {
262 Self::Reqwest(WrappedReqwestError::from(error))
263 }
264}
265
266impl From<reqwest_middleware::Error> for Error {
267 fn from(error: reqwest_middleware::Error) -> Self {
268 match error {
269 reqwest_middleware::Error::Middleware(error) => Self::ReqwestMiddlewareError(error),
270 reqwest_middleware::Error::Reqwest(error) => {
271 Self::Reqwest(WrappedReqwestError::from(error))
272 }
273 }
274 }
275}
276
277impl uv_errors::Hinted for Error {
278 fn hints(&self) -> uv_errors::Hints<'_> {
279 match self {
280 Self::Build(err) => err.hints(),
281 Self::Client(err) => uv_errors::Hinted::hints(err),
282 Self::MetadataLowering(err) => err.hints(),
283 _ => uv_errors::Hints::none(),
284 }
285 }
286}
287
288impl IsBuildBackendError for Error {
289 fn is_user_failure(&self) -> bool {
290 Self::is_user_failure(self)
291 }
292
293 fn is_build_backend_error(&self) -> bool {
294 match self {
295 Self::Build(err) => err.is_build_backend_error(),
296 _ => false,
297 }
298 }
299}
300
301impl Error {
302 pub fn is_user_failure(&self) -> bool {
304 match self {
305 Self::UnsupportedWheelVariant(_)
306 | Self::IncompatibleWheelVariant { .. }
307 | Self::NoBuild
308 | Self::NoBuildPackage(_)
309 | Self::InvalidUrl(_)
310 | Self::NonFileUrl(_)
311 | Self::WheelFilename(_)
312 | Self::WheelMetadataNameMismatch { .. }
313 | Self::WheelMetadataVersionMismatch { .. }
314 | Self::WheelFilenameNameMismatch { .. }
315 | Self::WheelFilenameVersionMismatch { .. }
316 | Self::BuiltWheelIncompatibleHostPlatform { .. }
317 | Self::BuiltWheelIncompatibleTargetPlatform { .. }
318 | Self::Metadata(_)
319 | Self::WheelMetadata(..)
320 | Self::Extract(..)
321 | Self::MissingPkgInfo
322 | Self::MissingSubdirectory(..)
323 | Self::MissingSourceDistGitLfsArtifacts(..)
324 | Self::MissingWheelGitLfsArtifacts(..)
325 | Self::PkgInfo(_)
326 | Self::MissingPyprojectToml
327 | Self::PyprojectToml(_)
328 | Self::MetadataLowering(_)
329 | Self::NotFound(_)
330 | Self::RequiresPython(..)
331 | Self::MismatchedHashes { .. }
332 | Self::MismatchedSize { .. }
333 | Self::MismatchedContentLength { .. }
334 | Self::MismatchedRangeSize { .. }
335 | Self::MissingHashes { .. }
336 | Self::MissingActualHashes { .. }
337 | Self::MissingExpectedHashes { .. }
338 | Self::HashesNotSupportedSourceTree(_)
339 | Self::HashesNotSupportedGit(_) => true,
340 Self::Build(error) => error.is_user_failure(),
341 Self::InstallWheelError(error) => error.is_user_failure(),
342 Self::Git(error) => error.is_user_failure(),
343 Self::Reqwest(error) => error.is_user_failure(),
344 Self::Client(error) => error.is_user_failure(),
345 Self::ClientBuild(error) => error.is_user_failure(),
346 Self::CacheRead(_)
347 | Self::CacheWrite(_)
348 | Self::CacheLock(_)
349 | Self::CacheDecode(_)
350 | Self::CacheEncode(_)
351 | Self::CacheWalk(_)
352 | Self::CacheInfo(_)
353 | Self::ReadInstalled(..)
354 | Self::CacheHeal(..)
355 | Self::BaseInterpreter(_)
356 | Self::Join(_)
357 | Self::HashExhaustion(_) => false,
358 Self::ReqwestMiddlewareError(error) => is_not_found_error(error.as_ref()),
359 }
360 }
361
362 pub fn hash_mismatch(
364 distribution: String,
365 expected: &[HashDigest],
366 actual: &[HashDigest],
367 ) -> Self {
368 match (expected.is_empty(), actual.is_empty()) {
369 (true, true) => Self::MissingHashes { distribution },
370 (true, false) => {
371 let actual = actual
372 .iter()
373 .map(|hash| format!(" {hash}"))
374 .collect::<Vec<_>>()
375 .join("\n");
376
377 Self::MissingExpectedHashes {
378 distribution,
379 actual,
380 }
381 }
382 (false, true) => {
383 let expected = expected
384 .iter()
385 .map(|hash| format!(" {hash}"))
386 .collect::<Vec<_>>()
387 .join("\n");
388
389 Self::MissingActualHashes {
390 distribution,
391 expected,
392 }
393 }
394 (false, false) => {
395 let expected = expected
396 .iter()
397 .map(|hash| format!(" {hash}"))
398 .collect::<Vec<_>>()
399 .join("\n");
400
401 let actual = actual
402 .iter()
403 .map(|hash| format!(" {hash}"))
404 .collect::<Vec<_>>()
405 .join("\n");
406
407 Self::MismatchedHashes {
408 distribution,
409 expected,
410 actual,
411 }
412 }
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::{Error, PythonVersion};
420 use std::str::FromStr;
421 use uv_distribution_filename::WheelFilename;
422 use uv_platform_tags::{Arch, Os, Platform};
423 use uv_python_types::PythonVariant;
424
425 #[test]
426 fn built_wheel_error_formats_freethreaded_python() {
427 let err = Error::BuiltWheelIncompatibleHostPlatform {
428 filename: WheelFilename::from_str(
429 "cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl",
430 )
431 .unwrap(),
432 python_platform: Platform::new(
433 Os::Macos {
434 major: 11,
435 minor: 0,
436 },
437 Arch::Aarch64,
438 ),
439 python_version: PythonVersion {
440 version: (3, 15),
441 variant: PythonVariant::Freethreaded,
442 },
443 };
444
445 assert_eq!(
446 err.to_string(),
447 "The built wheel `cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl` is not compatible with the current Python 3.15t on macOS aarch64"
448 );
449 }
450
451 #[test]
452 fn built_wheel_error_formats_target_python() {
453 let err = Error::BuiltWheelIncompatibleTargetPlatform {
454 filename: WheelFilename::from_str("py313-0.1.0-py313-none-any.whl").unwrap(),
455 python_platform: Platform::new(
456 Os::Manylinux {
457 major: 2,
458 minor: 28,
459 },
460 Arch::X86_64,
461 ),
462 python_version: PythonVersion {
463 version: (3, 12),
464 variant: PythonVariant::Default,
465 },
466 };
467
468 assert_eq!(
469 err.to_string(),
470 "The built wheel `py313-0.1.0-py313-none-any.whl` is not compatible with the target Python 3.12 on Linux x86_64. Consider using `--no-build` to disable building wheels."
471 );
472 }
473}