1use std::path::PathBuf;
2
3use owo_colors::OwoColorize;
4use tokio::task::JoinError;
5use zip::result::ZipError;
6
7use crate::metadata::MetadataError;
8use uv_client::WrappedReqwestError;
9use uv_distribution_filename::{WheelFilename, WheelFilenameError};
10use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendError};
11use uv_fs::{LockedFileError, Simplified};
12use uv_git::GitError;
13use uv_normalize::PackageName;
14use uv_pep440::{Version, VersionSpecifiers};
15use uv_platform_tags::Platform;
16use uv_pypi_types::{HashAlgorithm, HashDigest};
17use uv_redacted::DisplaySafeUrl;
18use uv_types::AnyErrorBuild;
19
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22 #[error("Building source distributions is disabled")]
23 NoBuild,
24
25 #[error("Expected an absolute path, but received: {}", _0.user_display())]
27 RelativePath(PathBuf),
28 #[error(transparent)]
29 InvalidUrl(#[from] uv_distribution_types::ToUrlError),
30 #[error("Expected a file URL, but received: {0}")]
31 NonFileUrl(DisplaySafeUrl),
32 #[error(transparent)]
33 Git(#[from] uv_git::GitResolverError),
34 #[error(transparent)]
35 Reqwest(#[from] WrappedReqwestError),
36 #[error(transparent)]
37 Client(#[from] uv_client::Error),
38
39 #[error("Failed to read from the distribution cache")]
41 CacheRead(#[source] std::io::Error),
42 #[error("Failed to write to the distribution cache")]
43 CacheWrite(#[source] std::io::Error),
44 #[error("Failed to acquire lock on the distribution cache")]
45 CacheLock(#[source] LockedFileError),
46 #[error("Failed to deserialize cache entry")]
47 CacheDecode(#[from] rmp_serde::decode::Error),
48 #[error("Failed to serialize cache entry")]
49 CacheEncode(#[from] rmp_serde::encode::Error),
50 #[error("Failed to walk the distribution cache")]
51 CacheWalk(#[source] walkdir::Error),
52 #[error(transparent)]
53 CacheInfo(#[from] uv_cache_info::CacheInfoError),
54
55 #[error(transparent)]
57 Build(AnyErrorBuild),
58 #[error("Built wheel has an invalid filename")]
59 WheelFilename(#[from] WheelFilenameError),
60 #[error("Package metadata name `{metadata}` does not match given name `{given}`")]
61 WheelMetadataNameMismatch {
62 given: PackageName,
63 metadata: PackageName,
64 },
65 #[error("Package metadata version `{metadata}` does not match given version `{given}`")]
66 WheelMetadataVersionMismatch { given: Version, metadata: Version },
67 #[error(
68 "Package metadata name `{metadata}` does not match `{filename}` from the wheel filename"
69 )]
70 WheelFilenameNameMismatch {
71 filename: PackageName,
72 metadata: PackageName,
73 },
74 #[error(
75 "Package metadata version `{metadata}` does not match `{filename}` from the wheel filename"
76 )]
77 WheelFilenameVersionMismatch {
78 filename: Version,
79 metadata: Version,
80 },
81 #[error(
83 "The built wheel `{}` is not compatible with the current Python {}.{} on {} {}",
84 filename,
85 python_version.0,
86 python_version.1,
87 python_platform.os(),
88 python_platform.arch(),
89 )]
90 BuiltWheelIncompatibleHostPlatform {
91 filename: WheelFilename,
92 python_platform: Platform,
93 python_version: (u8, u8),
94 },
95 #[error(
98 "The built wheel `{}` is not compatible with the target Python {}.{} on {} {}. Consider using `--no-build` to disable building wheels.",
99 filename,
100 python_version.0,
101 python_version.1,
102 python_platform.os(),
103 python_platform.arch(),
104 )]
105 BuiltWheelIncompatibleTargetPlatform {
106 filename: WheelFilename,
107 python_platform: Platform,
108 python_version: (u8, u8),
109 },
110 #[error("Failed to parse metadata from built wheel")]
111 Metadata(#[from] uv_pypi_types::MetadataError),
112 #[error("Failed to read metadata: `{}`", _0.user_display())]
113 WheelMetadata(PathBuf, #[source] Box<uv_metadata::Error>),
114 #[error("Failed to read metadata from installed package `{0}`")]
115 ReadInstalled(Box<InstalledDist>, #[source] InstalledDistError),
116 #[error("Failed to read zip archive from built wheel")]
117 Zip(#[from] ZipError),
118 #[error("Failed to extract archive: {0}")]
119 Extract(String, #[source] uv_extract::Error),
120 #[error("The source distribution is missing a `PKG-INFO` file")]
121 MissingPkgInfo,
122 #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())]
123 MissingSubdirectory(DisplaySafeUrl, PathBuf),
124 #[error("The source distribution `{0}` is missing Git LFS artifacts.")]
125 MissingGitLfsArtifacts(DisplaySafeUrl, #[source] GitError),
126 #[error("Failed to extract static metadata from `PKG-INFO`")]
127 PkgInfo(#[source] uv_pypi_types::MetadataError),
128 #[error("Failed to extract metadata from `requires.txt`")]
129 RequiresTxt(#[source] uv_pypi_types::MetadataError),
130 #[error("The source distribution is missing a `pyproject.toml` file")]
131 MissingPyprojectToml,
132 #[error("Failed to extract static metadata from `pyproject.toml`")]
133 PyprojectToml(#[source] uv_pypi_types::MetadataError),
134 #[error("Unsupported scheme in URL: {0}")]
135 UnsupportedScheme(String),
136 #[error(transparent)]
137 MetadataLowering(#[from] MetadataError),
138 #[error("Distribution not found at: {0}")]
139 NotFound(DisplaySafeUrl),
140 #[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())]
141 CacheHeal(String, HashAlgorithm),
142 #[error("The source distribution requires Python {0}, but {1} is installed")]
143 RequiresPython(VersionSpecifiers, Version),
144 #[error("Failed to identify base Python interpreter")]
145 BaseInterpreter(#[source] std::io::Error),
146
147 #[error(transparent)]
150 ReqwestMiddlewareError(#[from] anyhow::Error),
151
152 #[error("The task executor is broken, did some other task panic?")]
154 Join(#[from] JoinError),
155
156 #[error("Failed to hash distribution")]
158 HashExhaustion(#[source] std::io::Error),
159
160 #[error("Hash mismatch for `{distribution}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")]
161 MismatchedHashes {
162 distribution: String,
163 expected: String,
164 actual: String,
165 },
166
167 #[error(
168 "Hash-checking is enabled, but no hashes were provided or computed for: `{distribution}`"
169 )]
170 MissingHashes { distribution: String },
171
172 #[error(
173 "Hash-checking is enabled, but no hashes were computed for: `{distribution}`\n\nExpected:\n{expected}"
174 )]
175 MissingActualHashes {
176 distribution: String,
177 expected: String,
178 },
179
180 #[error(
181 "Hash-checking is enabled, but no hashes were provided for: `{distribution}`\n\nComputed:\n{actual}"
182 )]
183 MissingExpectedHashes {
184 distribution: String,
185 actual: String,
186 },
187
188 #[error("Hash-checking is not supported for local directories: `{0}`")]
189 HashesNotSupportedSourceTree(String),
190
191 #[error("Hash-checking is not supported for Git repositories: `{0}`")]
192 HashesNotSupportedGit(String),
193}
194
195impl From<reqwest::Error> for Error {
196 fn from(error: reqwest::Error) -> Self {
197 Self::Reqwest(WrappedReqwestError::from(error))
198 }
199}
200
201impl From<reqwest_middleware::Error> for Error {
202 fn from(error: reqwest_middleware::Error) -> Self {
203 match error {
204 reqwest_middleware::Error::Middleware(error) => Self::ReqwestMiddlewareError(error),
205 reqwest_middleware::Error::Reqwest(error) => {
206 Self::Reqwest(WrappedReqwestError::from(error))
207 }
208 }
209 }
210}
211
212impl IsBuildBackendError for Error {
213 fn is_build_backend_error(&self) -> bool {
214 match self {
215 Self::Build(err) => err.is_build_backend_error(),
216 _ => false,
217 }
218 }
219}
220
221impl Error {
222 pub fn hash_mismatch(
224 distribution: String,
225 expected: &[HashDigest],
226 actual: &[HashDigest],
227 ) -> Self {
228 match (expected.is_empty(), actual.is_empty()) {
229 (true, true) => Self::MissingHashes { distribution },
230 (true, false) => {
231 let actual = actual
232 .iter()
233 .map(|hash| format!(" {hash}"))
234 .collect::<Vec<_>>()
235 .join("\n");
236
237 Self::MissingExpectedHashes {
238 distribution,
239 actual,
240 }
241 }
242 (false, true) => {
243 let expected = expected
244 .iter()
245 .map(|hash| format!(" {hash}"))
246 .collect::<Vec<_>>()
247 .join("\n");
248
249 Self::MissingActualHashes {
250 distribution,
251 expected,
252 }
253 }
254 (false, false) => {
255 let expected = expected
256 .iter()
257 .map(|hash| format!(" {hash}"))
258 .collect::<Vec<_>>()
259 .join("\n");
260
261 let actual = actual
262 .iter()
263 .map(|hash| format!(" {hash}"))
264 .collect::<Vec<_>>()
265 .join("\n");
266
267 Self::MismatchedHashes {
268 distribution,
269 expected,
270 actual,
271 }
272 }
273 }
274 }
275}