Skip to main content

uv_resolver/resolver/
provider.rs

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