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 "Hash-checking is enabled, but no hashes were provided or computed for: `{distribution}`"
178 )]
179 MissingHashes { distribution: String },
180
181 #[error(
182 "Hash-checking is enabled, but no hashes were computed for: `{distribution}`\n\nExpected:\n{expected}"
183 )]
184 MissingActualHashes {
185 distribution: String,
186 expected: String,
187 },
188
189 #[error(
190 "Hash-checking is enabled, but no hashes were provided for: `{distribution}`\n\nComputed:\n{actual}"
191 )]
192 MissingExpectedHashes {
193 distribution: String,
194 actual: String,
195 },
196
197 #[error("Hash-checking is not supported for local directories: `{0}`")]
198 HashesNotSupportedSourceTree(String),
199
200 #[error("Hash-checking is not supported for Git repositories: `{0}`")]
201 HashesNotSupportedGit(String),
202
203 #[error(transparent)]
204 InstallWheelError(uv_install_wheel::Error),
205}
206
207impl From<reqwest::Error> for Error {
208 fn from(error: reqwest::Error) -> Self {
209 Self::Reqwest(WrappedReqwestError::from(error))
210 }
211}
212
213impl From<reqwest_middleware::Error> for Error {
214 fn from(error: reqwest_middleware::Error) -> Self {
215 match error {
216 reqwest_middleware::Error::Middleware(error) => Self::ReqwestMiddlewareError(error),
217 reqwest_middleware::Error::Reqwest(error) => {
218 Self::Reqwest(WrappedReqwestError::from(error))
219 }
220 }
221 }
222}
223
224impl uv_errors::Hint for Error {
225 fn hints(&self) -> uv_errors::Hints<'_> {
226 match self {
227 Self::Build(err) => err.hints(),
228 Self::Client(err) => uv_errors::Hint::hints(err),
229 Self::MetadataLowering(err) => err.hints(),
230 _ => uv_errors::Hints::none(),
231 }
232 }
233}
234
235impl IsBuildBackendError for Error {
236 fn is_build_backend_error(&self) -> bool {
237 match self {
238 Self::Build(err) => err.is_build_backend_error(),
239 _ => false,
240 }
241 }
242}
243
244impl Error {
245 pub fn hash_mismatch(
247 distribution: String,
248 expected: &[HashDigest],
249 actual: &[HashDigest],
250 ) -> Self {
251 match (expected.is_empty(), actual.is_empty()) {
252 (true, true) => Self::MissingHashes { distribution },
253 (true, false) => {
254 let actual = actual
255 .iter()
256 .map(|hash| format!(" {hash}"))
257 .collect::<Vec<_>>()
258 .join("\n");
259
260 Self::MissingExpectedHashes {
261 distribution,
262 actual,
263 }
264 }
265 (false, true) => {
266 let expected = expected
267 .iter()
268 .map(|hash| format!(" {hash}"))
269 .collect::<Vec<_>>()
270 .join("\n");
271
272 Self::MissingActualHashes {
273 distribution,
274 expected,
275 }
276 }
277 (false, false) => {
278 let expected = expected
279 .iter()
280 .map(|hash| format!(" {hash}"))
281 .collect::<Vec<_>>()
282 .join("\n");
283
284 let actual = actual
285 .iter()
286 .map(|hash| format!(" {hash}"))
287 .collect::<Vec<_>>()
288 .join("\n");
289
290 Self::MismatchedHashes {
291 distribution,
292 expected,
293 actual,
294 }
295 }
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::{Error, PythonVersion};
303 use std::str::FromStr;
304 use uv_distribution_filename::WheelFilename;
305 use uv_platform_tags::{Arch, Os, Platform};
306 use uv_python::PythonVariant;
307
308 #[test]
309 fn built_wheel_error_formats_freethreaded_python() {
310 let err = Error::BuiltWheelIncompatibleHostPlatform {
311 filename: WheelFilename::from_str(
312 "cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl",
313 )
314 .unwrap(),
315 python_platform: Platform::new(
316 Os::Macos {
317 major: 11,
318 minor: 0,
319 },
320 Arch::Aarch64,
321 ),
322 python_version: PythonVersion {
323 version: (3, 15),
324 variant: PythonVariant::Freethreaded,
325 },
326 };
327
328 assert_eq!(
329 err.to_string(),
330 "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"
331 );
332 }
333
334 #[test]
335 fn built_wheel_error_formats_target_python() {
336 let err = Error::BuiltWheelIncompatibleTargetPlatform {
337 filename: WheelFilename::from_str("py313-0.1.0-py313-none-any.whl").unwrap(),
338 python_platform: Platform::new(
339 Os::Manylinux {
340 major: 2,
341 minor: 28,
342 },
343 Arch::X86_64,
344 ),
345 python_version: PythonVersion {
346 version: (3, 12),
347 variant: PythonVariant::Default,
348 },
349 };
350
351 assert_eq!(
352 err.to_string(),
353 "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."
354 );
355 }
356}