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