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