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