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