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 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 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 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 fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
89        self.0.iter()
90    }
91
92    /// Removes the [`PrioritizedDist`] for the given version.
93    pub fn remove(&mut self, version: &Version) -> Option<PrioritizedDist> {
94        self.0.remove(version)
95    }
96
97    /// Add the given [`File`] to the [`FlatDistributions`] for the given package.
98    fn add_file(
99        &mut self,
100        file: File,
101        filename: DistFilename,
102        tags: Option<&Tags>,
103        hasher: &HashStrategy,
104        build_options: &BuildOptions,
105        index: IndexUrl,
106    ) {
107        // No `requires-python` here: for source distributions, we don't have that information;
108        // for wheels, we read it lazily only when selected.
109        match filename {
110            DistFilename::WheelFilename(filename) => {
111                let version = filename.version.clone();
112
113                let compatibility = Self::wheel_compatibility(
114                    &filename,
115                    file.hashes.as_slice(),
116                    tags,
117                    hasher,
118                    build_options,
119                );
120                let dist = RegistryBuiltWheel {
121                    filename,
122                    file: Box::new(file),
123                    index,
124                };
125                match self.0.entry(version) {
126                    Entry::Occupied(mut entry) => {
127                        entry.get_mut().insert_built(dist, vec![], compatibility);
128                    }
129                    Entry::Vacant(entry) => {
130                        entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility));
131                    }
132                }
133            }
134            DistFilename::SourceDistFilename(filename) => {
135                let compatibility = Self::source_dist_compatibility(
136                    &filename,
137                    file.hashes.as_slice(),
138                    hasher,
139                    build_options,
140                );
141                let dist = RegistrySourceDist {
142                    name: filename.name.clone(),
143                    version: filename.version.clone(),
144                    ext: filename.extension,
145                    file: Box::new(file),
146                    index,
147                    wheels: vec![],
148                };
149                match self.0.entry(filename.version) {
150                    Entry::Occupied(mut entry) => {
151                        entry.get_mut().insert_source(dist, vec![], compatibility);
152                    }
153                    Entry::Vacant(entry) => {
154                        entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility));
155                    }
156                }
157            }
158        }
159    }
160
161    fn source_dist_compatibility(
162        filename: &SourceDistFilename,
163        hashes: &[HashDigest],
164        hasher: &HashStrategy,
165        build_options: &BuildOptions,
166    ) -> SourceDistCompatibility {
167        // Check if source distributions are allowed for this package.
168        if build_options.no_build_package(&filename.name) {
169            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
170        }
171
172        // Check if hashes line up
173        let hash_policy = hasher.get_package(&filename.name, &filename.version);
174        let hash = if hash_policy.requires_validation() {
175            if hashes.is_empty() {
176                HashComparison::Missing
177            } else if hash_policy.matches(hashes) {
178                HashComparison::Matched
179            } else {
180                HashComparison::Mismatched
181            }
182        } else {
183            HashComparison::Matched
184        };
185
186        SourceDistCompatibility::Compatible(hash)
187    }
188
189    fn wheel_compatibility(
190        filename: &WheelFilename,
191        hashes: &[HashDigest],
192        tags: Option<&Tags>,
193        hasher: &HashStrategy,
194        build_options: &BuildOptions,
195    ) -> WheelCompatibility {
196        // Check if binaries are allowed for this package.
197        if build_options.no_binary_package(&filename.name) {
198            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
199        }
200
201        // Determine a compatibility for the wheel based on tags.
202        let priority = match tags {
203            Some(tags) => match filename.compatibility(tags) {
204                TagCompatibility::Incompatible(tag) => {
205                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
206                }
207                TagCompatibility::Compatible(priority) => Some(priority),
208            },
209            None => None,
210        };
211
212        // Check if hashes line up.
213        let hash_policy = hasher.get_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        // Break ties with the build tag.
227        let build_tag = filename.build_tag().cloned();
228
229        WheelCompatibility::Compatible(hash, priority, build_tag)
230    }
231}
232
233impl IntoIterator for FlatDistributions {
234    type Item = (Version, PrioritizedDist);
235    type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
236
237    fn into_iter(self) -> Self::IntoIter {
238        self.0.into_iter()
239    }
240}
241
242impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
243    fn from(distributions: FlatDistributions) -> Self {
244        distributions.0
245    }
246}
247
248/// For external users.
249impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
250    fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
251        Self(distributions)
252    }
253}