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