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::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: FlatIndex,
121    tags: Option<Tags>,
122    requires_python: RequiresPython,
123    allowed_yanks: AllowedYanks,
124    hasher: 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: flat_index.clone(),
149            tags: tags.cloned(),
150            requires_python: requires_python.clone(),
151            allowed_yanks,
152            hasher: hasher.clone(),
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
198        match result {
199            Ok(results) => Ok(VersionsResponse::Found(
200                results
201                    .into_iter()
202                    .map(|(index, metadata)| {
203                        let included_version_cutoff =
204                            self.effective_exclude_newer(package_name, index);
205                        let available_version_cutoff = included_version_cutoff
206                            .is_none()
207                            .then_some(self.available_version_cutoff)
208                            .flatten();
209
210                        match metadata {
211                            MetadataFormat::Simple(metadata) => VersionMap::from_simple_metadata(
212                                metadata,
213                                package_name,
214                                index,
215                                self.tags.as_ref(),
216                                &self.requires_python,
217                                &self.allowed_yanks,
218                                &self.hasher,
219                                included_version_cutoff,
220                                available_version_cutoff,
221                                flat_index
222                                    .and_then(|flat_index| flat_index.get(package_name))
223                                    .cloned(),
224                                self.build_options,
225                            ),
226                            MetadataFormat::Flat(metadata) => VersionMap::from_flat_metadata(
227                                metadata,
228                                self.tags.as_ref(),
229                                &self.hasher,
230                                self.build_options,
231                            ),
232                        }
233                    })
234                    .collect(),
235            )),
236            Err(err) => match err.kind() {
237                uv_client::ErrorKind::RemotePackageNotFound(_) => {
238                    if let Some(flat_index) = flat_index
239                        .and_then(|flat_index| flat_index.get(package_name))
240                        .cloned()
241                    {
242                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
243                    } else {
244                        Ok(VersionsResponse::NotFound)
245                    }
246                }
247                uv_client::ErrorKind::NoIndex(_) => {
248                    if let Some(flat_index) = flat_index
249                        .and_then(|flat_index| flat_index.get(package_name))
250                        .cloned()
251                    {
252                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
253                    } else if flat_index.is_some_and(FlatIndex::offline) {
254                        Ok(VersionsResponse::Offline)
255                    } else {
256                        Ok(VersionsResponse::NoIndex)
257                    }
258                }
259                uv_client::ErrorKind::Offline(_) => {
260                    if let Some(flat_index) = flat_index
261                        .and_then(|flat_index| flat_index.get(package_name))
262                        .cloned()
263                    {
264                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
265                    } else {
266                        Ok(VersionsResponse::Offline)
267                    }
268                }
269                _ => Err(err),
270            },
271        }
272    }
273
274    /// Fetch the metadata for a distribution, building it if necessary.
275    async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult {
276        match self
277            .fetcher
278            .get_or_build_wheel_metadata(dist, self.hasher.get(dist))
279            .await
280        {
281            Ok(metadata) => Ok(MetadataResponse::Found(metadata)),
282            Err(err) => match err {
283                uv_distribution::Error::Client(client) => {
284                    let retries = client.retries();
285                    let duration = client.duration();
286                    match client.into_kind() {
287                        uv_client::ErrorKind::Offline(_) => {
288                            Ok(MetadataResponse::Unavailable(MetadataUnavailable::Offline))
289                        }
290                        uv_client::ErrorKind::MetadataParseError(_, _, err) => {
291                            Ok(MetadataResponse::Unavailable(
292                                MetadataUnavailable::InvalidMetadata(Arc::new(*err)),
293                            ))
294                        }
295                        uv_client::ErrorKind::Metadata(_, err) => {
296                            Ok(MetadataResponse::Unavailable(
297                                MetadataUnavailable::InvalidStructure(Arc::new(err)),
298                            ))
299                        }
300                        uv_client::ErrorKind::WrappedReqwestError(url, err) => {
301                            let Some(status) = err.status().filter(|status| {
302                                dist.index().is_some_and(|index| {
303                                    self.index_locations.ignores_error_code_for(index, *status)
304                                })
305                            }) else {
306                                return Err(uv_client::Error::new(
307                                    uv_client::ErrorKind::WrappedReqwestError(url, err),
308                                    retries,
309                                    duration,
310                                )
311                                .into());
312                            };
313                            Ok(MetadataResponse::Unavailable(MetadataUnavailable::Network(
314                                status,
315                            )))
316                        }
317                        kind => Err(uv_client::Error::new(kind, retries, duration).into()),
318                    }
319                }
320                uv_distribution::Error::WheelMetadataVersionMismatch { .. } => {
321                    Ok(MetadataResponse::Unavailable(
322                        MetadataUnavailable::InconsistentMetadata(Arc::new(err)),
323                    ))
324                }
325                uv_distribution::Error::WheelMetadataNameMismatch { .. } => {
326                    Ok(MetadataResponse::Unavailable(
327                        MetadataUnavailable::InconsistentMetadata(Arc::new(err)),
328                    ))
329                }
330                uv_distribution::Error::Metadata(err) => Ok(MetadataResponse::Unavailable(
331                    MetadataUnavailable::InvalidMetadata(Arc::new(err)),
332                )),
333                uv_distribution::Error::WheelMetadata(_, err) => Ok(MetadataResponse::Unavailable(
334                    MetadataUnavailable::InvalidStructure(Arc::new(*err)),
335                )),
336                uv_distribution::Error::RequiresPython(requires_python, version) => {
337                    Ok(MetadataResponse::Unavailable(
338                        MetadataUnavailable::RequiresPython(requires_python, version),
339                    ))
340                }
341                err => Ok(MetadataResponse::Error(
342                    Box::new(RequestedDist::Installable(dist.clone())),
343                    Arc::new(err),
344                )),
345            },
346        }
347    }
348
349    /// Return the metadata for an installed distribution.
350    async fn get_installed_metadata<'io>(
351        &'io self,
352        dist: &'io InstalledDist,
353    ) -> WheelMetadataResult {
354        match self.fetcher.get_installed_metadata(dist).await {
355            Ok(metadata) => Ok(MetadataResponse::Found(metadata)),
356            Err(err) => Ok(MetadataResponse::Error(
357                Box::new(RequestedDist::Installed(dist.clone())),
358                Arc::new(err),
359            )),
360        }
361    }
362
363    /// Set the [`Reporter`] to use for this installer.
364    fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
365        Self {
366            fetcher: self.fetcher.with_reporter(reporter),
367            ..self
368        }
369    }
370}