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