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