Skip to main content

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