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