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