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