Skip to main content

uv_distribution/
error.rs

1use std::fmt;
2use std::path::PathBuf;
3
4use owo_colors::OwoColorize;
5use tokio::task::JoinError;
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_python::PythonVariant;
19use uv_redacted::DisplaySafeUrl;
20use uv_types::AnyErrorBuild;
21
22#[derive(Debug, Clone, Copy)]
23pub struct PythonVersion {
24    pub(crate) version: (u8, u8),
25    pub(crate) variant: PythonVariant,
26}
27
28impl fmt::Display for PythonVersion {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        let (major, minor) = self.version;
31        write!(f, "{major}.{minor}{}", self.variant.executable_suffix())
32    }
33}
34
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37    #[error("Building source distributions is disabled")]
38    NoBuild,
39    #[error("Building source distributions for `{0}` is disabled")]
40    NoBuildPackage(PackageName),
41
42    // Network error
43    #[error(transparent)]
44    InvalidUrl(#[from] uv_distribution_types::ToUrlError),
45    #[error("Expected a file URL, but received: {0}")]
46    NonFileUrl(DisplaySafeUrl),
47    #[error(transparent)]
48    Git(#[from] uv_git::GitResolverError),
49    #[error(transparent)]
50    Reqwest(#[from] WrappedReqwestError),
51    #[error(transparent)]
52    Client(#[from] uv_client::Error),
53
54    // Cache writing error
55    #[error("Failed to read from the distribution cache")]
56    CacheRead(#[source] std::io::Error),
57    #[error("Failed to write to the distribution cache")]
58    CacheWrite(#[source] std::io::Error),
59    #[error("Failed to acquire lock on the distribution cache")]
60    CacheLock(#[source] CacheError),
61    #[error("Failed to deserialize cache entry")]
62    CacheDecode(#[from] rmp_serde::decode::Error),
63    #[error("Failed to serialize cache entry")]
64    CacheEncode(#[from] rmp_serde::encode::Error),
65    #[error("Failed to walk the distribution cache")]
66    CacheWalk(#[source] walkdir::Error),
67    #[error(transparent)]
68    CacheInfo(#[from] uv_cache_info::CacheInfoError),
69
70    // Build error
71    #[error(transparent)]
72    Build(AnyErrorBuild),
73    #[error("Built wheel has an invalid filename")]
74    WheelFilename(#[from] WheelFilenameError),
75    #[error("Package metadata name `{metadata}` does not match given name `{given}`")]
76    WheelMetadataNameMismatch {
77        given: PackageName,
78        metadata: PackageName,
79    },
80    #[error("Package metadata version `{metadata}` does not match given version `{given}`")]
81    WheelMetadataVersionMismatch { given: Version, metadata: Version },
82    #[error(
83        "Package metadata name `{metadata}` does not match `{filename}` from the wheel filename"
84    )]
85    WheelFilenameNameMismatch {
86        filename: PackageName,
87        metadata: PackageName,
88    },
89    #[error(
90        "Package metadata version `{metadata}` does not match `{filename}` from the wheel filename"
91    )]
92    WheelFilenameVersionMismatch {
93        filename: Version,
94        metadata: Version,
95    },
96    /// This shouldn't happen, it's a bug in the build backend.
97    #[error(
98        "The built wheel `{}` is not compatible with the current Python {} on {}",
99        filename,
100        python_version,
101        python_platform.pretty(),
102    )]
103    BuiltWheelIncompatibleHostPlatform {
104        filename: WheelFilename,
105        python_platform: Platform,
106        python_version: PythonVersion,
107    },
108    /// This may happen when trying to cross-install native dependencies without their build backend
109    /// being aware that the target is a cross-install.
110    #[error(
111        "The built wheel `{}` is not compatible with the target Python {} on {}. Consider using `--no-build` to disable building wheels.",
112        filename,
113        python_version,
114        python_platform.pretty(),
115    )]
116    BuiltWheelIncompatibleTargetPlatform {
117        filename: WheelFilename,
118        python_platform: Platform,
119        python_version: PythonVersion,
120    },
121    #[error("Failed to parse metadata from built wheel")]
122    Metadata(#[from] uv_pypi_types::MetadataError),
123    #[error("Failed to read metadata: `{}`", _0.user_display())]
124    WheelMetadata(PathBuf, #[source] Box<uv_metadata::Error>),
125    #[error("Failed to read metadata from installed package `{0}`")]
126    ReadInstalled(Box<InstalledDist>, #[source] InstalledDistError),
127    #[error("Failed to extract archive: {0}")]
128    Extract(String, #[source] uv_extract::Error),
129    #[error("The source distribution is missing a `PKG-INFO` file")]
130    MissingPkgInfo,
131    #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())]
132    MissingSubdirectory(DisplaySafeUrl, PathBuf),
133    #[error("The source distribution `{0}` is missing Git LFS artifacts.")]
134    MissingGitLfsArtifacts(DisplaySafeUrl, #[source] GitError),
135    #[error("Failed to extract static metadata from `PKG-INFO`")]
136    PkgInfo(#[source] uv_pypi_types::MetadataError),
137    #[error("The source distribution is missing a `pyproject.toml` file")]
138    MissingPyprojectToml,
139    #[error("Failed to extract static metadata from `pyproject.toml`")]
140    PyprojectToml(#[source] uv_pypi_types::MetadataError),
141    #[error(transparent)]
142    MetadataLowering(#[from] MetadataError),
143    #[error("Distribution not found at: {0}")]
144    NotFound(DisplaySafeUrl),
145    #[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())]
146    CacheHeal(String, HashAlgorithm),
147    #[error("The source distribution requires Python {0}, but {1} is installed")]
148    RequiresPython(VersionSpecifiers, Version),
149    #[error("Failed to identify base Python interpreter")]
150    BaseInterpreter(#[source] std::io::Error),
151
152    /// A generic request middleware error happened while making a request.
153    /// Refer to the error message for more details.
154    #[error(transparent)]
155    ReqwestMiddlewareError(#[from] anyhow::Error),
156
157    /// Should not occur; only seen when another task panicked.
158    #[error("The task executor is broken, did some other task panic?")]
159    Join(#[from] JoinError),
160
161    /// An I/O error that occurs while exhausting a reader to compute a hash.
162    #[error("Failed to hash distribution")]
163    HashExhaustion(#[source] std::io::Error),
164
165    #[error("Hash mismatch for `{distribution}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")]
166    MismatchedHashes {
167        distribution: String,
168        expected: String,
169        actual: String,
170    },
171
172    #[error(
173        "Hash-checking is enabled, but no hashes were provided or computed for: `{distribution}`"
174    )]
175    MissingHashes { distribution: String },
176
177    #[error(
178        "Hash-checking is enabled, but no hashes were computed for: `{distribution}`\n\nExpected:\n{expected}"
179    )]
180    MissingActualHashes {
181        distribution: String,
182        expected: String,
183    },
184
185    #[error(
186        "Hash-checking is enabled, but no hashes were provided for: `{distribution}`\n\nComputed:\n{actual}"
187    )]
188    MissingExpectedHashes {
189        distribution: String,
190        actual: String,
191    },
192
193    #[error("Hash-checking is not supported for local directories: `{0}`")]
194    HashesNotSupportedSourceTree(String),
195
196    #[error("Hash-checking is not supported for Git repositories: `{0}`")]
197    HashesNotSupportedGit(String),
198
199    #[error(transparent)]
200    InstallWheelError(uv_install_wheel::Error),
201}
202
203impl From<reqwest::Error> for Error {
204    fn from(error: reqwest::Error) -> Self {
205        Self::Reqwest(WrappedReqwestError::from(error))
206    }
207}
208
209impl From<reqwest_middleware::Error> for Error {
210    fn from(error: reqwest_middleware::Error) -> Self {
211        match error {
212            reqwest_middleware::Error::Middleware(error) => Self::ReqwestMiddlewareError(error),
213            reqwest_middleware::Error::Reqwest(error) => {
214                Self::Reqwest(WrappedReqwestError::from(error))
215            }
216        }
217    }
218}
219
220impl IsBuildBackendError for Error {
221    fn is_build_backend_error(&self) -> bool {
222        match self {
223            Self::Build(err) => err.is_build_backend_error(),
224            _ => false,
225        }
226    }
227}
228
229impl Error {
230    /// Construct a hash mismatch error.
231    pub fn hash_mismatch(
232        distribution: String,
233        expected: &[HashDigest],
234        actual: &[HashDigest],
235    ) -> Self {
236        match (expected.is_empty(), actual.is_empty()) {
237            (true, true) => Self::MissingHashes { distribution },
238            (true, false) => {
239                let actual = actual
240                    .iter()
241                    .map(|hash| format!("  {hash}"))
242                    .collect::<Vec<_>>()
243                    .join("\n");
244
245                Self::MissingExpectedHashes {
246                    distribution,
247                    actual,
248                }
249            }
250            (false, true) => {
251                let expected = expected
252                    .iter()
253                    .map(|hash| format!("  {hash}"))
254                    .collect::<Vec<_>>()
255                    .join("\n");
256
257                Self::MissingActualHashes {
258                    distribution,
259                    expected,
260                }
261            }
262            (false, false) => {
263                let expected = expected
264                    .iter()
265                    .map(|hash| format!("  {hash}"))
266                    .collect::<Vec<_>>()
267                    .join("\n");
268
269                let actual = actual
270                    .iter()
271                    .map(|hash| format!("  {hash}"))
272                    .collect::<Vec<_>>()
273                    .join("\n");
274
275                Self::MismatchedHashes {
276                    distribution,
277                    expected,
278                    actual,
279                }
280            }
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::{Error, PythonVersion};
288    use std::str::FromStr;
289    use uv_distribution_filename::WheelFilename;
290    use uv_platform_tags::{Arch, Os, Platform};
291    use uv_python::PythonVariant;
292
293    #[test]
294    fn built_wheel_error_formats_freethreaded_python() {
295        let err = Error::BuiltWheelIncompatibleHostPlatform {
296            filename: WheelFilename::from_str(
297                "cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl",
298            )
299            .unwrap(),
300            python_platform: Platform::new(
301                Os::Macos {
302                    major: 11,
303                    minor: 0,
304                },
305                Arch::Aarch64,
306            ),
307            python_version: PythonVersion {
308                version: (3, 15),
309                variant: PythonVariant::Freethreaded,
310            },
311        };
312
313        assert_eq!(
314            err.to_string(),
315            "The built wheel `cryptography-47.0.0.dev1-cp315-abi3t-macosx_11_0_arm64.whl` is not compatible with the current Python 3.15t on macOS aarch64"
316        );
317    }
318
319    #[test]
320    fn built_wheel_error_formats_target_python() {
321        let err = Error::BuiltWheelIncompatibleTargetPlatform {
322            filename: WheelFilename::from_str("py313-0.1.0-py313-none-any.whl").unwrap(),
323            python_platform: Platform::new(
324                Os::Manylinux {
325                    major: 2,
326                    minor: 28,
327                },
328                Arch::X86_64,
329            ),
330            python_version: PythonVersion {
331                version: (3, 12),
332                variant: PythonVariant::Default,
333            },
334        };
335
336        assert_eq!(
337            err.to_string(),
338            "The built wheel `py313-0.1.0-py313-none-any.whl` is not compatible with the target Python 3.12 on Linux x86_64. Consider using `--no-build` to disable building wheels."
339        );
340    }
341}