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::{WheelFilename, WheelFilenameError};
11use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError};
12use uv_fs::Simplified;
13use uv_git::GitError;
14use uv_normalize::PackageName;
15use uv_pep440::{Version, VersionSpecifiers};
16use uv_platform_tags::Platform;
17use uv_pypi_types::{HashAlgorithm, HashDigest};
18use uv_python::PythonVariant;
19use uv_redacted::DisplaySafeUrl;
20use uv_types::AnyErrorBuild;
21
22#[derive(Debug, Clone, Copy)]
23pub struct PythonVersion {
24 pub(crate) version: (u8, u8),
25 pub(crate) variant: PythonVariant,
26}
27
28impl fmt::Display for PythonVersion {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 let (major, minor) = self.version;
31 write!(f, "{major}.{minor}{}", self.variant.executable_suffix())
32 }
33}
34
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37 #[error("Building source distributions is disabled")]
38 NoBuild,
39 #[error("Building source distributions for `{0}` is disabled")]
40 NoBuildPackage(PackageName),
41
42 #[error(transparent)]
44 InvalidUrl(#[from] uv_distribution_types::ToUrlError),
45 #[error("Expected a file URL, but received: {0}")]
46 NonFileUrl(DisplaySafeUrl),
47 #[error(transparent)]
48 Git(#[from] uv_git::GitResolverError),
49 #[error(transparent)]
50 Reqwest(#[from] WrappedReqwestError),
51 #[error(transparent)]
52 Client(#[from] uv_client::Error),
53 #[error(transparent)]
54 ClientBuild(#[from] uv_client::ClientBuildError),
55
56 #[error("Failed to read from the distribution cache")]
58 CacheRead(#[source] std::io::Error),
59 #[error("Failed to write to the distribution cache")]
60 CacheWrite(#[source] std::io::Error),
61 #[error("Failed to acquire lock on the distribution cache")]
62 CacheLock(#[source] CacheError),
63 #[error("Failed to deserialize cache entry")]
64 CacheDecode(#[from] rmp_serde::decode::Error),
65 #[error("Failed to serialize cache entry")]
66 CacheEncode(#[from] rmp_serde::encode::Error),
67 #[error("Failed to walk the distribution cache")]
68 CacheWalk(#[source] walkdir::Error),
69 #[error(transparent)]
70 CacheInfo(#[from] uv_cache_info::CacheInfoError),
71
72 #[error(transparent)]
74 Build(AnyErrorBuild),
75 #[error("Built wheel has an invalid filename")]
76 WheelFilename(#[from] WheelFilenameError),
77 #[error("Package metadata name `{metadata}` does not match given name `{given}`")]
78 WheelMetadataNameMismatch {
79 given: PackageName,
80 metadata: PackageName,
81 },
82 #[error("Package metadata version `{metadata}` does not match given version `{given}`")]
83 WheelMetadataVersionMismatch { given: Version, metadata: Version },
84 #[error(
85 "Package metadata name `{metadata}` does not match `{filename}` from the wheel filename"
86 )]
87 WheelFilenameNameMismatch {
88 filename: PackageName,
89 metadata: PackageName,
90 },
91 #[error(
92 "Package metadata version `{metadata}` does not match `{filename}` from the wheel filename"
93 )]
94 WheelFilenameVersionMismatch {
95 filename: Version,
96 metadata: Version,
97 },
98 #[error(
100 "The built wheel `{}` is not compatible with the current Python {} on {}",
101 filename,
102 python_version,
103 python_platform.pretty(),
104 )]
105 BuiltWheelIncompatibleHostPlatform {
106 filename: WheelFilename,
107 python_platform: Platform,
108 python_version: PythonVersion,
109 },
110 #[error(
113 "The built wheel `{}` is not compatible with the target Python {} on {}. Consider using `--no-build` to disable building wheels.",
114 filename,
115 python_version,
116 python_platform.pretty(),
117 )]
118 BuiltWheelIncompatibleTargetPlatform {
119 filename: WheelFilename,
120 python_platform: Platform,
121 python_version: PythonVersion,
122 },
123 #[error("Failed to parse metadata from built wheel")]
124 Metadata(#[from] uv_pypi_types::MetadataError),
125 #[error("Failed to read metadata: `{}`", _0.user_display())]
126 WheelMetadata(PathBuf, #[source] Box<uv_metadata::Error>),
127 #[error("Failed to read metadata from installed package `{0}`")]
128 ReadInstalled(Box<InstalledDist>, #[source] InstalledDistError),
129 #[error("Failed to extract archive: {0}")]
130 Extract(String, #[source] uv_extract::Error),
131 #[error("The source distribution is missing a `PKG-INFO` file")]
132 MissingPkgInfo,
133 #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())]
134 MissingSubdirectory(DisplaySafeUrl, PathBuf),
135 #[error("The source distribution `{0}` is missing Git LFS artifacts.")]
136 MissingSourceDistGitLfsArtifacts(DisplaySafeUrl, #[source] GitError),
137 #[error("The wheel `{0}` is missing Git LFS artifacts.")]
138 MissingWheelGitLfsArtifacts(DisplaySafeUrl, #[source] GitError),
139 #[error("Failed to extract static metadata from `PKG-INFO`")]
140 PkgInfo(#[source] uv_pypi_types::MetadataError),
141 #[error("The source distribution is missing a `pyproject.toml` file")]
142 MissingPyprojectToml,
143 #[error("Failed to extract static metadata from `pyproject.toml`")]
144 PyprojectToml(#[source] uv_pypi_types::MetadataError),
145 #[error(transparent)]
146 MetadataLowering(#[from] MetadataError),
147 #[error("Distribution not found at: {0}")]
148 NotFound(DisplaySafeUrl),
149 #[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())]
150 CacheHeal(String, HashAlgorithm),
151 #[error("The source distribution requires Python {0}, but {1} is installed")]
152 RequiresPython(VersionSpecifiers, Version),
153 #[error("Failed to identify base Python interpreter")]
154 BaseInterpreter(#[source] std::io::Error),
155
156 #[error(transparent)]
159 ReqwestMiddlewareError(#[from] anyhow::Error),
160
161 #[error("The task executor is broken, did some other task panic?")]
163 Join(#[from] JoinError),
164
165 #[error("Failed to hash distribution")]
167 HashExhaustion(#[source] std::io::Error),
168
169 #[error("Hash mismatch for `{distribution}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")]
170 MismatchedHashes {
171 distribution: String,
172 expected: String,
173 actual: String,
174 },
175
176 #[error(
177 "Size mismatch for `{distribution}`: expected {expected} bytes, but downloaded {actual} bytes"
178 )]
179 MismatchedSize {
180 distribution: String,
181 expected: u64,
182 actual: u64,
183 },
184
185 #[error(
186 "Hash-checking is enabled, but no hashes were provided or computed for: `{distribution}`"
187 )]
188 MissingHashes { distribution: String },
189
190 #[error(
191 "Hash-checking is enabled, but no hashes were computed for: `{distribution}`\n\nExpected:\n{expected}"
192 )]
193 MissingActualHashes {
194 distribution: String,
195 expected: String,
196 },
197
198 #[error(
199 "Hash-checking is enabled, but no hashes were provided for: `{distribution}`\n\nComputed:\n{actual}"
200 )]
201 MissingExpectedHashes {
202 distribution: String,
203 actual: String,
204 },
205
206 #[error("Hash-checking is not supported for local directories: `{0}`")]
207 HashesNotSupportedSourceTree(String),
208
209 #[error("Hash-checking is not supported for Git repositories: `{0}`")]
210 HashesNotSupportedGit(String),
211
212 #[error(transparent)]
213 InstallWheelError(uv_install_wheel::Error),
214}
215
216impl From<reqwest::Error> for Error {
217 fn from(error: reqwest::Error) -> Self {
218 Self::Reqwest(WrappedReqwestError::from(error))
219 }
220}
221
222impl From<reqwest_middleware::Error> for Error {
223 fn from(error: reqwest_middleware::Error) -> Self {
224 match error {
225 reqwest_middleware::Error::Middleware(error) => Self::ReqwestMiddlewareError(error),
226 reqwest_middleware::Error::Reqwest(error) => {
227 Self::Reqwest(WrappedReqwestError::from(error))
228 }
229 }
230 }
231}
232
233impl uv_errors::Hint for Error {
234 fn hints(&self) -> uv_errors::Hints<'_> {
235 match self {
236 Self::Build(err) => err.hints(),
237 Self::Client(err) => uv_errors::Hint::hints(err),
238 Self::MetadataLowering(err) => err.hints(),
239 _ => uv_errors::Hints::none(),
240 }
241 }
242}
243
244impl IsBuildBackendError for Error {
245 fn is_build_backend_error(&self) -> bool {
246 match self {
247 Self::Build(err) => err.is_build_backend_error(),
248 _ => false,
249 }
250 }
251}
252
253impl Error {
254 pub fn hash_mismatch(
256 distribution: String,
257 expected: &[HashDigest],
258 actual: &[HashDigest],
259 ) -> Self {
260 match (expected.is_empty(), actual.is_empty()) {
261 (true, true) => Self::MissingHashes { distribution },
262 (true, false) => {
263 let actual = actual
264 .iter()
265 .map(|hash| format!(" {hash}"))
266 .collect::<Vec<_>>()
267 .join("\n");
268
269 Self::MissingExpectedHashes {
270 distribution,
271 actual,
272 }
273 }
274 (false, true) => {
275 let expected = expected
276 .iter()
277 .map(|hash| format!(" {hash}"))
278 .collect::<Vec<_>>()
279 .join("\n");
280
281 Self::MissingActualHashes {
282 distribution,
283 expected,
284 }
285 }
286 (false, false) => {
287 let expected = expected
288 .iter()
289 .map(|hash| format!(" {hash}"))
290 .collect::<Vec<_>>()
291 .join("\n");
292
293 let actual = actual
294 .iter()
295 .map(|hash| format!(" {hash}"))
296 .collect::<Vec<_>>()
297 .join("\n");
298
299 Self::MismatchedHashes {
300 distribution,
301 expected,
302 actual,
303 }
304 }
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::{Error, PythonVersion};
312 use std::str::FromStr;
313 use uv_distribution_filename::WheelFilename;
314 use uv_platform_tags::{Arch, Os, Platform};
315 use uv_python::PythonVariant;
316
317 #[test]
318 fn built_wheel_error_formats_freethreaded_python() {
319 let err = Error::BuiltWheelIncompatibleHostPlatform {
320 filename: WheelFilename::from_str(
321 "cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl",
322 )
323 .unwrap(),
324 python_platform: Platform::new(
325 Os::Macos {
326 major: 11,
327 minor: 0,
328 },
329 Arch::Aarch64,
330 ),
331 python_version: PythonVersion {
332 version: (3, 15),
333 variant: PythonVariant::Freethreaded,
334 },
335 };
336
337 assert_eq!(
338 err.to_string(),
339 "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"
340 );
341 }
342
343 #[test]
344 fn built_wheel_error_formats_target_python() {
345 let err = Error::BuiltWheelIncompatibleTargetPlatform {
346 filename: WheelFilename::from_str("py313-0.1.0-py313-none-any.whl").unwrap(),
347 python_platform: Platform::new(
348 Os::Manylinux {
349 major: 2,
350 minor: 28,
351 },
352 Arch::X86_64,
353 ),
354 python_version: PythonVersion {
355 version: (3, 12),
356 variant: PythonVariant::Default,
357 },
358 };
359
360 assert_eq!(
361 err.to_string(),
362 "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."
363 );
364 }
365}