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                    size_is_authoritative: false,
120                };
121                match self.0.entry(version) {
122                    Entry::Occupied(mut entry) => {
123                        entry.get_mut().insert_built(dist, vec![], compatibility);
124                    }
125                    Entry::Vacant(entry) => {
126                        entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility));
127                    }
128                }
129            }
130            DistFilename::SourceDistFilename(filename) => {
131                let compatibility = Self::source_dist_compatibility(
132                    &filename,
133                    file.hashes.as_slice(),
134                    hasher,
135                    build_options,
136                );
137                let dist = RegistrySourceDist {
138                    name: filename.name.clone(),
139                    version: filename.version.clone(),
140                    ext: filename.extension,
141                    file: Box::new(file),
142                    index,
143                    wheels: vec![],
144                    size_is_authoritative: false,
145                };
146                match self.0.entry(filename.version) {
147                    Entry::Occupied(mut entry) => {
148                        entry.get_mut().insert_source(dist, vec![], compatibility);
149                    }
150                    Entry::Vacant(entry) => {
151                        entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility));
152                    }
153                }
154            }
155        }
156    }
157
158    fn source_dist_compatibility(
159        filename: &SourceDistFilename,
160        hashes: &[HashDigest],
161        hasher: &HashStrategy,
162        build_options: &BuildOptions,
163    ) -> SourceDistCompatibility {
164        // Check if source distributions are allowed for this package.
165        if build_options.no_build_package(&filename.name) {
166            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
167        }
168
169        // Check if the filename is PEP 625-compliant.
170        // TODO: Strengthen this check more; right now we allow `.zip`
171        // (which is not compliant) and we don't strictly
172        // enforce the formatting rules for the name or version.
173        if !filename.extension.is_pep625_compliant() {
174            return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
175        }
176
177        // Check if hashes line up
178        let hash_policy = hasher.get_package(&filename.name, &filename.version);
179        let hash = if hash_policy.requires_validation() {
180            if hashes.is_empty() {
181                HashComparison::Missing
182            } else if hash_policy.matches(hashes) {
183                HashComparison::Matched
184            } else {
185                HashComparison::Mismatched
186            }
187        } else {
188            HashComparison::Matched
189        };
190
191        SourceDistCompatibility::Compatible(hash)
192    }
193
194    fn wheel_compatibility(
195        filename: &WheelFilename,
196        hashes: &[HashDigest],
197        tags: Option<&Tags>,
198        hasher: &HashStrategy,
199        build_options: &BuildOptions,
200    ) -> WheelCompatibility {
201        // Check if binaries are allowed for this package.
202        if build_options.no_binary_package(&filename.name) {
203            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
204        }
205
206        // Determine a compatibility for the wheel based on tags.
207        let priority = match tags {
208            Some(tags) => match filename.compatibility(tags) {
209                TagCompatibility::Incompatible(tag) => {
210                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
211                }
212                TagCompatibility::Compatible(priority) => Some(priority),
213            },
214            None => None,
215        };
216
217        // Check if hashes line up.
218        let hash_policy = hasher.get_package(&filename.name, &filename.version);
219        let hash = if hash_policy.requires_validation() {
220            if hashes.is_empty() {
221                HashComparison::Missing
222            } else if hash_policy.matches(hashes) {
223                HashComparison::Matched
224            } else {
225                HashComparison::Mismatched
226            }
227        } else {
228            HashComparison::Matched
229        };
230
231        // Break ties with the build tag.
232        let build_tag = filename.build_tag().cloned();
233
234        WheelCompatibility::Compatible(hash, priority, build_tag)
235    }
236}
237
238impl IntoIterator for FlatDistributions {
239    type Item = (Version, PrioritizedDist);
240    type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
241
242    fn into_iter(self) -> Self::IntoIter {
243        self.0.into_iter()
244    }
245}
246
247impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
248    fn from(distributions: FlatDistributions) -> Self {
249        distributions.0
250    }
251}
252
253/// For external users.
254impl From<BTreeMap<Version, PrioritizedDist>> for FlatDistributions {
255    fn from(distributions: BTreeMap<Version, PrioritizedDist>) -> Self {
256        Self(distributions)
257    }
258}