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