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::MetadataLowering(err) => err.hints(),
229 _ => uv_errors::Hints::none(),
230 }
231 }
232}
233
234impl IsBuildBackendError for Error {
235 fn is_build_backend_error(&self) -> bool {
236 match self {
237 Self::Build(err) => err.is_build_backend_error(),
238 _ => false,
239 }
240 }
241}
242
243impl Error {
244 pub fn hash_mismatch(
246 distribution: String,
247 expected: &[HashDigest],
248 actual: &[HashDigest],
249 ) -> Self {
250 match (expected.is_empty(), actual.is_empty()) {
251 (true, true) => Self::MissingHashes { distribution },
252 (true, false) => {
253 let actual = actual
254 .iter()
255 .map(|hash| format!(" {hash}"))
256 .collect::<Vec<_>>()
257 .join("\n");
258
259 Self::MissingExpectedHashes {
260 distribution,
261 actual,
262 }
263 }
264 (false, true) => {
265 let expected = expected
266 .iter()
267 .map(|hash| format!(" {hash}"))
268 .collect::<Vec<_>>()
269 .join("\n");
270
271 Self::MissingActualHashes {
272 distribution,
273 expected,
274 }
275 }
276 (false, false) => {
277 let expected = expected
278 .iter()
279 .map(|hash| format!(" {hash}"))
280 .collect::<Vec<_>>()
281 .join("\n");
282
283 let actual = actual
284 .iter()
285 .map(|hash| format!(" {hash}"))
286 .collect::<Vec<_>>()
287 .join("\n");
288
289 Self::MismatchedHashes {
290 distribution,
291 expected,
292 actual,
293 }
294 }
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::{Error, PythonVersion};
302 use std::str::FromStr;
303 use uv_distribution_filename::WheelFilename;
304 use uv_platform_tags::{Arch, Os, Platform};
305 use uv_python::PythonVariant;
306
307 #[test]
308 fn built_wheel_error_formats_freethreaded_python() {
309 let err = Error::BuiltWheelIncompatibleHostPlatform {
310 filename: WheelFilename::from_str(
311 "cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl",
312 )
313 .unwrap(),
314 python_platform: Platform::new(
315 Os::Macos {
316 major: 11,
317 minor: 0,
318 },
319 Arch::Aarch64,
320 ),
321 python_version: PythonVersion {
322 version: (3, 15),
323 variant: PythonVariant::Freethreaded,
324 },
325 };
326
327 assert_eq!(
328 err.to_string(),
329 "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"
330 );
331 }
332
333 #[test]
334 fn built_wheel_error_formats_target_python() {
335 let err = Error::BuiltWheelIncompatibleTargetPlatform {
336 filename: WheelFilename::from_str("py313-0.1.0-py313-none-any.whl").unwrap(),
337 python_platform: Platform::new(
338 Os::Manylinux {
339 major: 2,
340 minor: 28,
341 },
342 Arch::X86_64,
343 ),
344 python_version: PythonVersion {
345 version: (3, 12),
346 variant: PythonVariant::Default,
347 },
348 };
349
350 assert_eq!(
351 err.to_string(),
352 "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."
353 );
354 }
355}