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