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