Skip to main content

uv_resolver/
flat_index.rs

1use std::collections::BTreeMap;
2
3use rustc_hash::FxHashMap;
4use tracing::instrument;
5
6use uv_cache::Cache;
7use uv_client::{
8    FlatIndexClient, FlatIndexEntries, FlatIndexEntry, FlatIndexError, RegistryClient,
9};
10use uv_configuration::BuildOptions;
11use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
12use uv_distribution_types::{
13    File, Index, IndexEntryFilename, IndexLocations, IndexUrl, MinimumLibcVersion,
14    RegistryBuiltWheel, RegistrySourceDist,
15};
16use uv_normalize::PackageName;
17use uv_pep440::Version;
18use uv_platform_tags::{IncompatibleTag, TagCompatibility, Tags};
19use uv_pypi_types::HashDigest;
20use uv_types::HashStrategy;
21
22use crate::prioritized_distribution::{
23    HashComparison, IncompatibleSource, IncompatibleWheel, PrioritizedDist, RegistryVariantsJson,
24    SourceDistCompatibility, WheelCompatibility,
25};
26
27/// Unfiltered entries from `--find-links`, indexed by [`PackageName`].
28#[derive(Debug, Clone, Default)]
29pub struct FlatIndex {
30    /// Entries are ranked using each resolver's platform and hash policies when requested.
31    index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
32    /// Whether any `--find-links` entries could not be resolved due to a lack of network
33    /// connectivity.
34    offline: bool,
35}
36
37impl FlatIndex {
38    /// Load the `--find-links` entries from the configured indexes.
39    pub async fn load(
40        client: &RegistryClient,
41        cache: &Cache,
42        index_locations: &IndexLocations,
43    ) -> Result<Self, FlatIndexError> {
44        let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
45        let entries = client
46            .fetch_all(index_locations.flat_indexes().map(Index::url))
47            .await?;
48        Ok(Self::from_entries(entries))
49    }
50
51    /// Collect all files from a `--find-links` target into a [`FlatIndex`].
52    #[instrument(skip_all)]
53    fn from_entries(entries: FlatIndexEntries) -> Self {
54        let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
55        let (entries, offline) = entries.into_parts();
56
57        for entry in entries {
58            index
59                .entry(entry.filename().name().clone())
60                .or_default()
61                .push(entry);
62        }
63
64        Self { index, offline }
65    }
66
67    /// Get the unfiltered entries for the given package name.
68    pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
69        self.index.get(package_name).map(Vec::as_slice)
70    }
71
72    /// Whether any `--find-links` entries could not be resolved due to a lack of network
73    /// connectivity.
74    pub(crate) fn offline(&self) -> bool {
75        self.offline
76    }
77}
78
79/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed
80/// by [`Version`].
81#[derive(Debug, Clone, Default)]
82pub struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
83
84impl FlatDistributions {
85    /// Rank the entries for a package using the resolver's platform and hash policies.
86    #[instrument(skip_all)]
87    pub(crate) fn from_entries(
88        entries: impl IntoIterator<Item = FlatIndexEntry>,
89        tags: Option<&Tags>,
90        hasher: &HashStrategy,
91        build_options: &BuildOptions,
92        minimum_libc_version: Option<MinimumLibcVersion>,
93    ) -> Self {
94        let mut distributions = Self::default();
95        for entry in entries {
96            let (filename, file, index) = entry.into_parts();
97            match filename {
98                IndexEntryFilename::DistFilename(filename) => distributions.add_file(
99                    file,
100                    filename,
101                    tags,
102                    hasher,
103                    build_options,
104                    index,
105                    minimum_libc_version,
106                ),
107                IndexEntryFilename::VariantJson(filename) => {
108                    distributions
109                        .0
110                        .entry(filename.version.clone())
111                        .or_default()
112                        .insert_variants_json(RegistryVariantsJson {
113                            filename,
114                            file,
115                            index,
116                        });
117                }
118            }
119        }
120        // Metadata alone does not make a package version installable.
121        distributions.0.retain(|_, dist| !dist.is_empty());
122        distributions
123    }
124
125    /// Returns an [`Iterator`] over the distributions.
126    pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
127        self.0.iter()
128    }
129
130    /// Add the given [`File`] to the [`FlatDistributions`] for the given package.
131    fn add_file(
132        &mut self,
133        file: File,
134        filename: DistFilename,
135        tags: Option<&Tags>,
136        hasher: &HashStrategy,
137        build_options: &BuildOptions,
138        index: IndexUrl,
139        minimum_libc_version: Option<MinimumLibcVersion>,
140    ) {
141        // No `requires-python` here: for source distributions, we don't have that information;
142        // for wheels, we read it lazily only when selected.
143        match filename {
144            DistFilename::WheelFilename(filename) => {
145                let version = filename.version.clone();
146
147                let compatibility = Self::wheel_compatibility(
148                    &filename,
149                    file.hashes.as_slice(),
150                    tags,
151                    hasher,
152                    build_options,
153                );
154                let dist = RegistryBuiltWheel {
155                    filename,
156                    file: Box::new(file),
157                    index,
158                    size_is_authoritative: false,
159                };
160                self.0.entry(version).or_default().insert_built(
161                    dist,
162                    vec![],
163                    compatibility,
164                    minimum_libc_version,
165                );
166            }
167            DistFilename::SourceDistFilename(filename) => {
168                let compatibility = Self::source_dist_compatibility(
169                    &filename,
170                    file.hashes.as_slice(),
171                    hasher,
172                    build_options,
173                );
174                let dist = RegistrySourceDist {
175                    name: filename.name.clone(),
176                    version: filename.version.clone(),
177                    ext: filename.extension,
178                    file: Box::new(file),
179                    index,
180                    wheels: vec![],
181                    size_is_authoritative: false,
182                };
183                self.0.entry(filename.version).or_default().insert_source(
184                    dist,
185                    vec![],
186                    compatibility,
187                );
188            }
189        }
190    }
191
192    fn source_dist_compatibility(
193        filename: &SourceDistFilename,
194        hashes: &[HashDigest],
195        hasher: &HashStrategy,
196        build_options: &BuildOptions,
197    ) -> SourceDistCompatibility {
198        // Check if source distributions are allowed for this package.
199        if build_options.no_build_package(&filename.name) {
200            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
201        }
202
203        // Check if the filename is PEP 625-compliant.
204        // TODO: Strengthen this check more; right now we allow `.zip`
205        // (which is not compliant) and we don't strictly
206        // enforce the formatting rules for the name or version.
207        if !filename.extension.is_pep625_compliant() {
208            return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
209        }
210
211        // Check if hashes line up
212        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
213        let hash = if hash_policy.requires_validation() {
214            if hashes.is_empty() {
215                HashComparison::Missing
216            } else if hash_policy.matches(hashes) {
217                HashComparison::Matched
218            } else {
219                HashComparison::Mismatched
220            }
221        } else {
222            HashComparison::Matched
223        };
224
225        SourceDistCompatibility::Compatible(hash)
226    }
227
228    fn wheel_compatibility(
229        filename: &WheelFilename,
230        hashes: &[HashDigest],
231        tags: Option<&Tags>,
232        hasher: &HashStrategy,
233        build_options: &BuildOptions,
234    ) -> WheelCompatibility {
235        // TODO(konsti): Check variant compatibility for flat-index wheels.
236        if filename.variant().is_some() {
237            return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(
238                IncompatibleTag::Variant,
239            ));
240        }
241        // Check if binaries are allowed for this package.
242        if build_options.no_binary_package(&filename.name) {
243            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
244        }
245
246        // Determine a compatibility for the wheel based on tags.
247        let priority = match tags {
248            Some(tags) => match filename.tag_compatibility(tags) {
249                TagCompatibility::Incompatible(tag) => {
250                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
251                }
252                TagCompatibility::Compatible(priority) => Some(priority),
253            },
254            None => None,
255        };
256
257        // Check if hashes line up.
258        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
259        let hash = if hash_policy.requires_validation() {
260            if hashes.is_empty() {
261                HashComparison::Missing
262            } else if hash_policy.matches(hashes) {
263                HashComparison::Matched
264            } else {
265                HashComparison::Mismatched
266            }
267        } else {
268            HashComparison::Matched
269        };
270
271        // Break ties with the build tag.
272        let build_tag = filename.build_tag().cloned();
273
274        WheelCompatibility::Compatible(hash, priority, build_tag)
275    }
276}
277
278impl IntoIterator for FlatDistributions {
279    type Item = (Version, PrioritizedDist);
280    type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
281
282    fn into_iter(self) -> Self::IntoIter {
283        self.0.into_iter()
284    }
285}
286
287impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
288    fn from(distributions: FlatDistributions) -> Self {
289        distributions.0
290    }
291}
292
293impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
294    fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
295        Self(distributions)
296    }
297}