Skip to main content

uv_resolver/resolver/
provider.rs

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