Skip to main content

uv_resolver/resolver/
provider.rs

1use std::future::Future;
2use std::sync::Arc;
3
4use reqwest::StatusCode;
5
6use uv_client::MetadataFormat;
7use uv_configuration::BuildOptions;
8use uv_distribution::{ArchiveMetadata, DistributionDatabase, Reporter};
9use uv_distribution_types::{
10    Dist, IndexCapabilities, IndexLocations, IndexMetadata, IndexMetadataRef, InstalledDist,
11    RequestedDist, RequiresPython,
12};
13use uv_normalize::PackageName;
14use uv_pep440::{Version, VersionSpecifiers};
15use uv_platform_tags::Tags;
16use uv_static::EnvVars;
17use uv_types::{BuildContext, HashStrategy};
18
19use crate::ExcludeNewer;
20use crate::flat_index::{FlatDistributions, FlatIndex};
21use crate::version_map::VersionMap;
22use crate::yanks::AllowedYanks;
23
24pub type PackageVersionsResult = Result<VersionsResponse, uv_client::Error>;
25pub type WheelMetadataResult = Result<MetadataResponse, uv_distribution::Error>;
26
27/// The response when requesting versions for a package
28#[derive(Debug)]
29pub enum VersionsResponse {
30    /// The package was found in the registry with the included versions
31    Found(Vec<VersionMap>),
32    /// The package was not found in the registry
33    NotFound,
34    /// The package was not found in the local registry
35    NoIndex,
36    /// The package was not found in the cache and the network is not available.
37    Offline,
38}
39
40#[derive(Debug)]
41pub enum MetadataResponse {
42    /// The wheel metadata was found and parsed successfully.
43    Found(ArchiveMetadata),
44    /// A non-fatal error.
45    Unavailable(MetadataUnavailable),
46    /// The distribution could not be built or downloaded, a fatal error.
47    Error(Box<RequestedDist>, Arc<uv_distribution::Error>),
48}
49
50/// Non-fatal metadata fetching error.
51///
52/// This is also the unavailability reasons for a package, while version unavailability is separate
53/// in [`UnavailableVersion`].
54#[derive(Debug, Clone)]
55pub enum MetadataUnavailable {
56    /// The wheel metadata was not found in the cache and the network is not available.
57    Offline,
58    /// The wheel metadata was found, but could not be parsed.
59    InvalidMetadata(Arc<uv_pypi_types::MetadataError>),
60    /// The wheel metadata was found, but the metadata was inconsistent.
61    InconsistentMetadata(Arc<uv_distribution::Error>),
62    /// The wheel has an invalid structure.
63    InvalidStructure(Arc<uv_metadata::Error>),
64    /// The source distribution has a `requires-python` requirement that is not met by the installed
65    /// Python version (and static metadata is not available).
66    RequiresPython(VersionSpecifiers, Version),
67    /// The wheel metadata could not be fetched due to a network error.
68    Network(StatusCode),
69}
70
71impl MetadataUnavailable {
72    /// Like [`std::error::Error::source`], but we don't want to derive the std error since our
73    /// formatting system is more custom.
74    pub(crate) fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
75        match self {
76            Self::Offline => None,
77            Self::InvalidMetadata(err) => Some(err),
78            Self::InconsistentMetadata(err) => Some(err),
79            Self::InvalidStructure(err) => Some(err),
80            Self::RequiresPython(..) | Self::Network(..) => None,
81        }
82    }
83}
84
85pub trait ResolverProvider {
86    /// Get the version map for a package.
87    fn get_package_versions<'io>(
88        &'io self,
89        package_name: &'io PackageName,
90        index: Option<&'io IndexMetadata>,
91    ) -> impl Future<Output = PackageVersionsResult> + 'io;
92
93    /// Get the metadata for a distribution.
94    ///
95    /// For a wheel, this is done by querying it (remote) metadata. For a source distribution, we
96    /// (fetch and) build the source distribution and return the metadata from the built
97    /// distribution.
98    fn get_or_build_wheel_metadata<'io>(
99        &'io self,
100        dist: &'io Dist,
101    ) -> impl Future<Output = WheelMetadataResult> + 'io;
102
103    /// Get the metadata for an installed distribution.
104    fn get_installed_metadata<'io>(
105        &'io self,
106        dist: &'io InstalledDist,
107    ) -> impl Future<Output = WheelMetadataResult> + 'io;
108
109    /// Set the [`Reporter`] to use for this installer.
110    #[must_use]
111    fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self;
112}
113
114/// The main IO backend for the resolver, which does cached requests network requests using the
115/// [`RegistryClient`] and [`DistributionDatabase`].
116pub struct DefaultResolverProvider<'a, Context: BuildContext> {
117    /// The [`DistributionDatabase`] used to build source distributions.
118    fetcher: DistributionDatabase<'a, Context>,
119    /// These are the entries from `--find-links` that act as overrides for index responses.
120    flat_index: &'a FlatIndex,
121    tags: Option<Tags>,
122    requires_python: RequiresPython,
123    allowed_yanks: AllowedYanks,
124    hasher: &'a HashStrategy,
125    exclude_newer: ExcludeNewer,
126    available_version_cutoff: Option<jiff::Timestamp>,
127    index_locations: &'a IndexLocations,
128    build_options: &'a BuildOptions,
129    capabilities: &'a IndexCapabilities,
130}
131
132impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> {
133    /// Reads the flat index entries and builds the provider.
134    pub fn new(
135        fetcher: DistributionDatabase<'a, Context>,
136        flat_index: &'a FlatIndex,
137        tags: Option<&'a Tags>,
138        requires_python: &'a RequiresPython,
139        allowed_yanks: AllowedYanks,
140        hasher: &'a HashStrategy,
141        exclude_newer: ExcludeNewer,
142        index_locations: &'a IndexLocations,
143        build_options: &'a BuildOptions,
144        capabilities: &'a IndexCapabilities,
145    ) -> Self {
146        Self {
147            fetcher,
148            flat_index,
149            tags: tags.cloned(),
150            requires_python: requires_python.clone(),
151            allowed_yanks,
152            hasher,
153            exclude_newer,
154            available_version_cutoff: std::env::var(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF)
155                .ok()
156                .and_then(|value| value.parse().ok()),
157            index_locations,
158            build_options,
159            capabilities,
160        }
161    }
162
163    fn effective_exclude_newer(
164        &self,
165        package_name: &PackageName,
166        index: &uv_distribution_types::IndexUrl,
167    ) -> Option<jiff::Timestamp> {
168        self.exclude_newer.exclude_newer_package_for_index(
169            package_name,
170            self.index_locations.exclude_newer_for(index),
171        )
172    }
173}
174
175impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Context> {
176    /// Make a "Simple API" request for the package and convert the result to a [`VersionMap`].
177    async fn get_package_versions<'io>(
178        &'io self,
179        package_name: &'io PackageName,
180        index: Option<&'io IndexMetadata>,
181    ) -> PackageVersionsResult {
182        let result = self
183            .fetcher
184            .client()
185            .manual(|client, semaphore| {
186                client.simple_detail(
187                    package_name,
188                    index.map(IndexMetadataRef::from),
189                    self.capabilities,
190                    semaphore,
191                )
192            })
193            .await;
194
195        // If a package is pinned to an explicit index, ignore any `--find-links` entries.
196        let flat_index = index.is_none().then_some(self.flat_index);
197        let flat_distributions = flat_index
198            .and_then(|flat_index| flat_index.get(package_name))
199            .map(|entries| {
200                FlatDistributions::from_entries(
201                    entries.iter().cloned(),
202                    self.tags.as_ref(),
203                    self.hasher,
204                    self.build_options,
205                )
206            });
207
208        match result {
209            Ok(results) => Ok(VersionsResponse::Found(
210                results
211                    .into_iter()
212                    .map(|(index, metadata)| {
213                        let included_version_cutoff =
214                            self.effective_exclude_newer(package_name, index);
215                        let available_version_cutoff = included_version_cutoff
216                            .is_none()
217                            .then_some(self.available_version_cutoff)
218                            .flatten();
219
220                        match metadata {
221                            MetadataFormat::Simple(metadata) => VersionMap::from_simple_metadata(
222                                metadata,
223                                package_name,
224                                index.clone(),
225                                self.tags.clone(),
226                                self.requires_python.clone(),
227                                self.allowed_yanks.clone(),
228                                self.hasher.clone(),
229                                included_version_cutoff,
230                                available_version_cutoff,
231                                flat_distributions.clone(),
232                                self.build_options,
233                            ),
234                            MetadataFormat::Flat(metadata) => VersionMap::from_flat_metadata(
235                                metadata,
236                                self.tags.as_ref(),
237                                self.hasher,
238                                self.build_options,
239                            ),
240                        }
241                    })
242                    .collect(),
243            )),
244            Err(err) => match err.kind() {
245                uv_client::ErrorKind::RemotePackageNotFound(_) => {
246                    if let Some(flat_index) = flat_distributions {
247                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
248                    } else {
249                        Ok(VersionsResponse::NotFound)
250                    }
251                }
252                uv_client::ErrorKind::NoIndex(_) => {
253                    if let Some(flat_index) = flat_distributions {
254                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
255                    } else if flat_index.is_some_and(FlatIndex::offline) {
256                        Ok(VersionsResponse::Offline)
257                    } else {
258                        Ok(VersionsResponse::NoIndex)
259                    }
260                }
261                uv_client::ErrorKind::Offline(_) => {
262                    if let Some(flat_index) = flat_distributions {
263                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
264                    } else {
265                        Ok(VersionsResponse::Offline)
266                    }
267                }
268                _ => Err(err),
269            },
270        }
271    }
272
273    /// Fetch the metadata for a distribution, building it if necessary.
274    async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult {
275        match self
276            .fetcher
277            .get_or_build_wheel_metadata(dist, self.hasher.get(dist))
278            .await
279        {
280            Ok(metadata) => Ok(MetadataResponse::Found(metadata)),
281            Err(err) => match err {
282                uv_distribution::Error::Client(client) => {
283                    let retries = client.retries();
284                    let duration = client.duration();
285                    match client.into_kind() {
286                        uv_client::ErrorKind::Offline(_) => {
287                            Ok(MetadataResponse::Unavailable(MetadataUnavailable::Offline))
288                        }
289                        uv_client::ErrorKind::MetadataParseError(_, _, err) => {
290                            Ok(MetadataResponse::Unavailable(
291                                MetadataUnavailable::InvalidMetadata(Arc::new(*err)),
292                            ))
293                        }
294                        uv_client::ErrorKind::Metadata(_, err) => {
295                            Ok(MetadataResponse::Unavailable(
296                                MetadataUnavailable::InvalidStructure(Arc::new(err)),
297                            ))
298                        }
299                        uv_client::ErrorKind::WrappedReqwestError(url, err) => {
300                            let Some(status) = err.status().filter(|status| {
301                                dist.index().is_some_and(|index| {
302                                    self.index_locations.ignores_error_code_for(index, *status)
303                                })
304                            }) else {
305                                return Err(uv_client::Error::new(
306                                    uv_client::ErrorKind::WrappedReqwestError(url, err),
307                                    retries,
308                                    duration,
309                                )
310                                .into());
311                            };
312                            Ok(MetadataResponse::Unavailable(MetadataUnavailable::Network(
313                                status,
314                            )))
315                        }
316                        kind => Err(uv_client::Error::new(kind, retries, duration).into()),
317                    }
318                }
319                uv_distribution::Error::WheelMetadataVersionMismatch { .. } => {
320                    Ok(MetadataResponse::Unavailable(
321                        MetadataUnavailable::InconsistentMetadata(Arc::new(err)),
322                    ))
323                }
324                uv_distribution::Error::WheelMetadataNameMismatch { .. } => {
325                    Ok(MetadataResponse::Unavailable(
326                        MetadataUnavailable::InconsistentMetadata(Arc::new(err)),
327                    ))
328                }
329                uv_distribution::Error::Metadata(err) => Ok(MetadataResponse::Unavailable(
330                    MetadataUnavailable::InvalidMetadata(Arc::new(err)),
331                )),
332                uv_distribution::Error::WheelMetadata(_, err) => Ok(MetadataResponse::Unavailable(
333                    MetadataUnavailable::InvalidStructure(Arc::new(*err)),
334                )),
335                uv_distribution::Error::RequiresPython(requires_python, version) => {
336                    Ok(MetadataResponse::Unavailable(
337                        MetadataUnavailable::RequiresPython(requires_python, version),
338                    ))
339                }
340                err => Ok(MetadataResponse::Error(
341                    Box::new(RequestedDist::Installable(dist.clone())),
342                    Arc::new(err),
343                )),
344            },
345        }
346    }
347
348    /// Return the metadata for an installed distribution.
349    async fn get_installed_metadata<'io>(
350        &'io self,
351        dist: &'io InstalledDist,
352    ) -> WheelMetadataResult {
353        match self.fetcher.get_installed_metadata(dist).await {
354            Ok(metadata) => Ok(MetadataResponse::Found(metadata)),
355            Err(err) => Ok(MetadataResponse::Error(
356                Box::new(RequestedDist::Installed(dist.clone())),
357                Arc::new(err),
358            )),
359        }
360    }
361
362    /// Set the [`Reporter`] to use for this installer.
363    fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
364        Self {
365            fetcher: self.fetcher.with_reporter(reporter),
366            ..self
367        }
368    }
369}