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