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