Skip to main content

uv_resolver/
flat_index.rs

1use std::collections::BTreeMap;
2use std::collections::btree_map::Entry;
3
4use rustc_hash::FxHashMap;
5use tracing::instrument;
6
7use uv_client::{FlatIndexEntries, FlatIndexEntry};
8use uv_configuration::BuildOptions;
9use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
10use uv_distribution_types::{
11    File, HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist,
12    RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility, WheelCompatibility,
13};
14use uv_normalize::PackageName;
15use uv_pep440::Version;
16use uv_platform_tags::{TagCompatibility, Tags};
17use uv_pypi_types::HashDigest;
18use uv_types::HashStrategy;
19
20/// Unfiltered entries from `--find-links`, indexed by [`PackageName`].
21#[derive(Debug, Clone, Default)]
22pub struct FlatIndex {
23    /// Entries are ranked using each resolver's platform and hash policies when requested.
24    index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
25    /// Whether any `--find-links` entries could not be resolved due to a lack of network
26    /// connectivity.
27    offline: bool,
28}
29
30impl FlatIndex {
31    /// Collect all files from a `--find-links` target into a [`FlatIndex`].
32    #[instrument(skip_all)]
33    pub fn from_entries(entries: FlatIndexEntries) -> Self {
34        let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
35        let (entries, offline) = entries.into_parts();
36
37        for entry in entries {
38            index
39                .entry(entry.filename().name().clone())
40                .or_default()
41                .push(entry);
42        }
43
44        Self { index, offline }
45    }
46
47    /// Get the unfiltered entries for the given package name.
48    pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
49        self.index.get(package_name).map(Vec::as_slice)
50    }
51
52    /// Whether any `--find-links` entries could not be resolved due to a lack of network
53    /// connectivity.
54    pub(crate) fn offline(&self) -> bool {
55        self.offline
56    }
57}
58
59/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed
60/// by [`Version`].
61#[derive(Debug, Clone, Default)]
62pub(crate) struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
63
64impl FlatDistributions {
65    /// Rank the entries for a package using the resolver's platform and hash policies.
66    #[instrument(skip_all)]
67    pub(crate) fn from_entries(
68        entries: impl IntoIterator<Item = FlatIndexEntry>,
69        tags: Option<&Tags>,
70        hasher: &HashStrategy,
71        build_options: &BuildOptions,
72    ) -> Self {
73        let mut distributions = Self::default();
74        for entry in entries {
75            let (filename, file, index) = entry.into_parts();
76            distributions.add_file(file, filename, tags, hasher, build_options, index);
77        }
78        distributions
79    }
80
81    /// Returns an [`Iterator`] over the distributions.
82    pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
83        self.0.iter()
84    }
85
86    /// Add the given [`File`] to the [`FlatDistributions`] for the given package.
87    fn add_file(
88        &mut self,
89        file: File,
90        filename: DistFilename,
91        tags: Option<&Tags>,
92        hasher: &HashStrategy,
93        build_options: &BuildOptions,
94        index: IndexUrl,
95    ) {
96        // No `requires-python` here: for source distributions, we don't have that information;
97        // for wheels, we read it lazily only when selected.
98        match filename {
99            DistFilename::WheelFilename(filename) => {
100                let version = filename.version.clone();
101
102                let compatibility = Self::wheel_compatibility(
103                    &filename,
104                    file.hashes.as_slice(),
105                    tags,
106                    hasher,
107                    build_options,
108                );
109                let dist = RegistryBuiltWheel {
110                    filename,
111                    file: Box::new(file),
112                    index,
113                    size_is_authoritative: false,
114                };
115                match self.0.entry(version) {
116                    Entry::Occupied(mut entry) => {
117                        entry.get_mut().insert_built(dist, vec![], compatibility);
118                    }
119                    Entry::Vacant(entry) => {
120                        entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility));
121                    }
122                }
123            }
124            DistFilename::SourceDistFilename(filename) => {
125                let compatibility = Self::source_dist_compatibility(
126                    &filename,
127                    file.hashes.as_slice(),
128                    hasher,
129                    build_options,
130                );
131                let dist = RegistrySourceDist {
132                    name: filename.name.clone(),
133                    version: filename.version.clone(),
134                    ext: filename.extension,
135                    file: Box::new(file),
136                    index,
137                    wheels: vec![],
138                    size_is_authoritative: false,
139                };
140                match self.0.entry(filename.version) {
141                    Entry::Occupied(mut entry) => {
142                        entry.get_mut().insert_source(dist, vec![], compatibility);
143                    }
144                    Entry::Vacant(entry) => {
145                        entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility));
146                    }
147                }
148            }
149        }
150    }
151
152    fn source_dist_compatibility(
153        filename: &SourceDistFilename,
154        hashes: &[HashDigest],
155        hasher: &HashStrategy,
156        build_options: &BuildOptions,
157    ) -> SourceDistCompatibility {
158        // Check if source distributions are allowed for this package.
159        if build_options.no_build_package(&filename.name) {
160            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
161        }
162
163        // Check if the filename is PEP 625-compliant.
164        // TODO: Strengthen this check more; right now we allow `.zip`
165        // (which is not compliant) and we don't strictly
166        // enforce the formatting rules for the name or version.
167        if !filename.extension.is_pep625_compliant() {
168            return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
169        }
170
171        // Check if hashes line up
172        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
173        let hash = if hash_policy.requires_validation() {
174            if hashes.is_empty() {
175                HashComparison::Missing
176            } else if hash_policy.matches(hashes) {
177                HashComparison::Matched
178            } else {
179                HashComparison::Mismatched
180            }
181        } else {
182            HashComparison::Matched
183        };
184
185        SourceDistCompatibility::Compatible(hash)
186    }
187
188    fn wheel_compatibility(
189        filename: &WheelFilename,
190        hashes: &[HashDigest],
191        tags: Option<&Tags>,
192        hasher: &HashStrategy,
193        build_options: &BuildOptions,
194    ) -> WheelCompatibility {
195        // Check if binaries are allowed for this package.
196        if build_options.no_binary_package(&filename.name) {
197            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
198        }
199
200        // Determine a compatibility for the wheel based on tags.
201        let priority = match tags {
202            Some(tags) => match filename.compatibility(tags) {
203                TagCompatibility::Incompatible(tag) => {
204                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
205                }
206                TagCompatibility::Compatible(priority) => Some(priority),
207            },
208            None => None,
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        // Break ties with the build tag.
226        let build_tag = filename.build_tag().cloned();
227
228        WheelCompatibility::Compatible(hash, priority, build_tag)
229    }
230}
231
232impl IntoIterator for FlatDistributions {
233    type Item = (Version, PrioritizedDist);
234    type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
235
236    fn into_iter(self) -> Self::IntoIter {
237        self.0.into_iter()
238    }
239}
240
241impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
242    fn from(distributions: FlatDistributions) -> Self {
243        distributions.0
244    }
245}