uv_distribution/
error.rs

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    // Network error
27    #[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    // Cache writing error
41    #[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    // Build error
57    #[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    /// This shouldn't happen, it's a bug in the build backend.
83    #[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    /// This may happen when trying to cross-install native dependencies without their build backend
97    /// being aware that the target is a cross-install.
98    #[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    /// A generic request middleware error happened while making a request.
149    /// Refer to the error message for more details.
150    #[error(transparent)]
151    ReqwestMiddlewareError(#[from] anyhow::Error),
152
153    /// Should not occur; only seen when another task panicked.
154    #[error("The task executor is broken, did some other task panic?")]
155    Join(#[from] JoinError),
156
157    /// An I/O error that occurs while exhausting a reader to compute a hash.
158    #[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    /// Construct a hash mismatch error.
224    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}