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