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    MinimumLibcVersion, 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    minimum_libc_version: Option<MinimumLibcVersion>,
85}
86
87impl<'a, Context: BuildContext> DefaultResolverProvider<'a, Context> {
88    /// Reads the flat index entries and builds the provider.
89    pub fn new(
90        fetcher: DistributionDatabase<'a, Context>,
91        flat_index: &'a FlatIndex,
92        tags: Option<&'a Tags>,
93        requires_python: &'a RequiresPython,
94        allowed_yanks: AllowedYanks,
95        hasher: &'a HashStrategy,
96        exclude_newer: ExcludeNewer,
97        index_locations: &'a IndexLocations,
98        build_options: &'a BuildOptions,
99        capabilities: &'a IndexCapabilities,
100        minimum_libc_version: Option<MinimumLibcVersion>,
101    ) -> Self {
102        Self {
103            fetcher,
104            flat_index,
105            tags: tags.cloned(),
106            requires_python: requires_python.clone(),
107            allowed_yanks,
108            hasher,
109            exclude_newer,
110            available_version_cutoff: std::env::var(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF)
111                .ok()
112                .and_then(|value| value.parse().ok()),
113            index_locations,
114            build_options,
115            capabilities,
116            minimum_libc_version,
117        }
118    }
119
120    fn effective_exclude_newer(
121        &self,
122        package_name: &PackageName,
123        index: &uv_distribution_types::IndexUrl,
124    ) -> Option<jiff::Timestamp> {
125        self.exclude_newer.exclude_newer_package_for_index(
126            package_name,
127            self.index_locations.exclude_newer_for(index),
128        )
129    }
130}
131
132impl<Context: BuildContext> ResolverProvider for DefaultResolverProvider<'_, Context> {
133    /// Make a "Simple API" request for the package and convert the result to a [`VersionMap`].
134    async fn get_package_versions<'io>(
135        &'io self,
136        package_name: &'io PackageName,
137        index: Option<&'io IndexMetadata>,
138    ) -> PackageVersionsResult {
139        let result = self
140            .fetcher
141            .client()
142            .manual(|client, semaphore| {
143                client.simple_detail(
144                    package_name,
145                    index.map(IndexMetadataRef::from),
146                    self.capabilities,
147                    semaphore,
148                )
149            })
150            .await;
151
152        // If a package is pinned to an explicit index, ignore any `--find-links` entries.
153        let flat_index = index.is_none().then_some(self.flat_index);
154        let flat_distributions = flat_index
155            .and_then(|flat_index| flat_index.get(package_name))
156            .map(|entries| {
157                FlatDistributions::from_entries(
158                    entries.iter().cloned(),
159                    self.tags.as_ref(),
160                    self.hasher,
161                    self.build_options,
162                    self.minimum_libc_version,
163                )
164            });
165
166        match result {
167            Ok(results) => Ok(VersionsResponse::Found(
168                results
169                    .into_iter()
170                    .map(|(index, metadata)| {
171                        let included_version_cutoff =
172                            self.effective_exclude_newer(package_name, index);
173                        let available_version_cutoff = included_version_cutoff
174                            .is_none()
175                            .then_some(self.available_version_cutoff)
176                            .flatten();
177
178                        match metadata {
179                            MetadataFormat::Simple(metadata) => VersionMap::from_simple_metadata(
180                                metadata,
181                                package_name,
182                                index.clone(),
183                                self.tags.clone(),
184                                self.requires_python.clone(),
185                                self.allowed_yanks.clone(),
186                                self.hasher.clone(),
187                                included_version_cutoff,
188                                available_version_cutoff,
189                                flat_distributions.clone(),
190                                self.build_options,
191                                self.minimum_libc_version,
192                            ),
193                            MetadataFormat::Flat(metadata) => VersionMap::from_flat_metadata(
194                                metadata,
195                                self.tags.as_ref(),
196                                self.hasher,
197                                self.build_options,
198                                self.minimum_libc_version,
199                            ),
200                        }
201                    })
202                    .collect(),
203            )),
204            Err(err) => match err.kind() {
205                uv_client::ErrorKind::RemotePackageNotFound(_) => {
206                    if let Some(flat_index) = flat_distributions {
207                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
208                    } else {
209                        Ok(VersionsResponse::NotFound)
210                    }
211                }
212                uv_client::ErrorKind::NoIndex(_) => {
213                    if let Some(flat_index) = flat_distributions {
214                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
215                    } else if flat_index.is_some_and(FlatIndex::offline) {
216                        Ok(VersionsResponse::Offline)
217                    } else {
218                        Ok(VersionsResponse::NoIndex)
219                    }
220                }
221                uv_client::ErrorKind::Offline(_) => {
222                    if let Some(flat_index) = flat_distributions {
223                        Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
224                    } else {
225                        Ok(VersionsResponse::Offline)
226                    }
227                }
228                _ => Err(err),
229            },
230        }
231    }
232
233    /// Fetch the metadata for a distribution, building it if necessary.
234    async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult {
235        match self
236            .fetcher
237            .get_or_build_wheel_metadata(dist, self.hasher.metadata_policy(dist))
238            .await
239        {
240            Ok(metadata) => Ok(MetadataResponse::Found(metadata)),
241            Err(err) => match err {
242                uv_distribution::Error::Client(client) => {
243                    let retries = client.retries();
244                    let duration = client.duration();
245                    match client.into_kind() {
246                        uv_client::ErrorKind::Offline(_) => {
247                            Ok(MetadataResponse::Unavailable(MetadataUnavailable::Offline))
248                        }
249                        uv_client::ErrorKind::MetadataParseError(_, _, err) => {
250                            Ok(MetadataResponse::Unavailable(
251                                MetadataUnavailable::InvalidMetadata(Arc::new(*err)),
252                            ))
253                        }
254                        uv_client::ErrorKind::Metadata(_, err) => {
255                            Ok(MetadataResponse::Unavailable(
256                                MetadataUnavailable::InvalidStructure(Arc::new(err)),
257                            ))
258                        }
259                        uv_client::ErrorKind::WrappedReqwestError(url, err) => {
260                            let Some(status) = err.status().filter(|status| {
261                                dist.index().is_some_and(|index| {
262                                    self.index_locations.ignores_error_code_for(index, *status)
263                                })
264                            }) else {
265                                return Err(uv_client::Error::new(
266                                    uv_client::ErrorKind::WrappedReqwestError(url, err),
267                                    retries,
268                                    duration,
269                                )
270                                .into());
271                            };
272                            Ok(MetadataResponse::Unavailable(MetadataUnavailable::Network(
273                                status,
274                            )))
275                        }
276                        kind => Err(uv_client::Error::new(kind, retries, duration).into()),
277                    }
278                }
279                uv_distribution::Error::WheelMetadataVersionMismatch { .. } => {
280                    Ok(MetadataResponse::Unavailable(
281                        MetadataUnavailable::InconsistentMetadata(Arc::new(err)),
282                    ))
283                }
284                uv_distribution::Error::WheelMetadataNameMismatch { .. } => {
285                    Ok(MetadataResponse::Unavailable(
286                        MetadataUnavailable::InconsistentMetadata(Arc::new(err)),
287                    ))
288                }
289                uv_distribution::Error::Metadata(err) => Ok(MetadataResponse::Unavailable(
290                    MetadataUnavailable::InvalidMetadata(Arc::new(err)),
291                )),
292                uv_distribution::Error::WheelMetadata(_, err) => Ok(MetadataResponse::Unavailable(
293                    MetadataUnavailable::InvalidStructure(Arc::new(*err)),
294                )),
295                uv_distribution::Error::RequiresPython(requires_python, version) => {
296                    Ok(MetadataResponse::Unavailable(
297                        MetadataUnavailable::RequiresPython(requires_python, version),
298                    ))
299                }
300                err => Ok(MetadataResponse::Error(
301                    Box::new(RequestedDist::Installable(dist.clone())),
302                    Arc::new(err),
303                )),
304            },
305        }
306    }
307
308    /// Return the metadata for an installed distribution.
309    async fn get_installed_metadata<'io>(
310        &'io self,
311        dist: &'io InstalledDist,
312    ) -> WheelMetadataResult {
313        match self.fetcher.get_installed_metadata(dist).await {
314            Ok(metadata) => Ok(MetadataResponse::Found(metadata)),
315            Err(err) => Ok(MetadataResponse::Error(
316                Box::new(RequestedDist::Installed(dist.clone())),
317                Arc::new(err),
318            )),
319        }
320    }
321
322    /// Set the [`Reporter`] to use for this installer.
323    fn with_reporter(self, reporter: Arc<dyn Reporter>) -> Self {
324        Self {
325            fetcher: self.fetcher.with_reporter(reporter),
326            ..self
327        }
328    }
329}