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