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