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_cache::Cache;
8use uv_client::{
9    FlatIndexClient, FlatIndexEntries, FlatIndexEntry, FlatIndexError, RegistryClient,
10};
11use uv_configuration::BuildOptions;
12use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
13use uv_distribution_types::{
14    File, HashComparison, IncompatibleSource, IncompatibleWheel, Index, IndexLocations, IndexUrl,
15    PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility,
16    WheelCompatibility,
17};
18use uv_normalize::PackageName;
19use uv_pep440::Version;
20use uv_platform_tags::{TagCompatibility, Tags};
21use uv_pypi_types::HashDigest;
22use uv_types::HashStrategy;
23
24/// Unfiltered entries from `--find-links`, indexed by [`PackageName`].
25#[derive(Debug, Clone, Default)]
26pub struct FlatIndex {
27    /// Entries are ranked using each resolver's platform and hash policies when requested.
28    index: FxHashMap<PackageName, Vec<FlatIndexEntry>>,
29    /// Whether any `--find-links` entries could not be resolved due to a lack of network
30    /// connectivity.
31    offline: bool,
32}
33
34impl FlatIndex {
35    /// Load the `--find-links` entries from the configured indexes.
36    pub async fn load(
37        client: &RegistryClient,
38        cache: &Cache,
39        index_locations: &IndexLocations,
40    ) -> Result<Self, FlatIndexError> {
41        let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
42        let entries = client
43            .fetch_all(index_locations.flat_indexes().map(Index::url))
44            .await?;
45        Ok(Self::from_entries(entries))
46    }
47
48    /// Collect all files from a `--find-links` target into a [`FlatIndex`].
49    #[instrument(skip_all)]
50    fn from_entries(entries: FlatIndexEntries) -> Self {
51        let mut index = FxHashMap::<PackageName, Vec<FlatIndexEntry>>::default();
52        let (entries, offline) = entries.into_parts();
53
54        for entry in entries {
55            index
56                .entry(entry.filename().name().clone())
57                .or_default()
58                .push(entry);
59        }
60
61        Self { index, offline }
62    }
63
64    /// Get the unfiltered entries for the given package name.
65    pub(crate) fn get(&self, package_name: &PackageName) -> Option<&[FlatIndexEntry]> {
66        self.index.get(package_name).map(Vec::as_slice)
67    }
68
69    /// Whether any `--find-links` entries could not be resolved due to a lack of network
70    /// connectivity.
71    pub(crate) fn offline(&self) -> bool {
72        self.offline
73    }
74}
75
76/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed
77/// by [`Version`].
78#[derive(Debug, Clone, Default)]
79pub(crate) struct FlatDistributions(BTreeMap<Version, PrioritizedDist>);
80
81impl FlatDistributions {
82    /// Rank the entries for a package using the resolver's platform and hash policies.
83    #[instrument(skip_all)]
84    pub(crate) fn from_entries(
85        entries: impl IntoIterator<Item = FlatIndexEntry>,
86        tags: Option<&Tags>,
87        hasher: &HashStrategy,
88        build_options: &BuildOptions,
89    ) -> Self {
90        let mut distributions = Self::default();
91        for entry in entries {
92            let (filename, file, index) = entry.into_parts();
93            distributions.add_file(file, filename, tags, hasher, build_options, index);
94        }
95        distributions
96    }
97
98    /// Returns an [`Iterator`] over the distributions.
99    pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
100        self.0.iter()
101    }
102
103    /// Add the given [`File`] to the [`FlatDistributions`] for the given package.
104    fn add_file(
105        &mut self,
106        file: File,
107        filename: DistFilename,
108        tags: Option<&Tags>,
109        hasher: &HashStrategy,
110        build_options: &BuildOptions,
111        index: IndexUrl,
112    ) {
113        // No `requires-python` here: for source distributions, we don't have that information;
114        // for wheels, we read it lazily only when selected.
115        match filename {
116            DistFilename::WheelFilename(filename) => {
117                let version = filename.version.clone();
118
119                let compatibility = Self::wheel_compatibility(
120                    &filename,
121                    file.hashes.as_slice(),
122                    tags,
123                    hasher,
124                    build_options,
125                );
126                let dist = RegistryBuiltWheel {
127                    filename,
128                    file: Box::new(file),
129                    index,
130                    size_is_authoritative: false,
131                };
132                match self.0.entry(version) {
133                    Entry::Occupied(mut entry) => {
134                        entry.get_mut().insert_built(dist, vec![], compatibility);
135                    }
136                    Entry::Vacant(entry) => {
137                        entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility));
138                    }
139                }
140            }
141            DistFilename::SourceDistFilename(filename) => {
142                let compatibility = Self::source_dist_compatibility(
143                    &filename,
144                    file.hashes.as_slice(),
145                    hasher,
146                    build_options,
147                );
148                let dist = RegistrySourceDist {
149                    name: filename.name.clone(),
150                    version: filename.version.clone(),
151                    ext: filename.extension,
152                    file: Box::new(file),
153                    index,
154                    wheels: vec![],
155                    size_is_authoritative: false,
156                };
157                match self.0.entry(filename.version) {
158                    Entry::Occupied(mut entry) => {
159                        entry.get_mut().insert_source(dist, vec![], compatibility);
160                    }
161                    Entry::Vacant(entry) => {
162                        entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility));
163                    }
164                }
165            }
166        }
167    }
168
169    fn source_dist_compatibility(
170        filename: &SourceDistFilename,
171        hashes: &[HashDigest],
172        hasher: &HashStrategy,
173        build_options: &BuildOptions,
174    ) -> SourceDistCompatibility {
175        // Check if source distributions are allowed for this package.
176        if build_options.no_build_package(&filename.name) {
177            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
178        }
179
180        // Check if the filename is PEP 625-compliant.
181        // TODO: Strengthen this check more; right now we allow `.zip`
182        // (which is not compliant) and we don't strictly
183        // enforce the formatting rules for the name or version.
184        if !filename.extension.is_pep625_compliant() {
185            return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
186        }
187
188        // Check if hashes line up
189        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
190        let hash = if hash_policy.requires_validation() {
191            if hashes.is_empty() {
192                HashComparison::Missing
193            } else if hash_policy.matches(hashes) {
194                HashComparison::Matched
195            } else {
196                HashComparison::Mismatched
197            }
198        } else {
199            HashComparison::Matched
200        };
201
202        SourceDistCompatibility::Compatible(hash)
203    }
204
205    fn wheel_compatibility(
206        filename: &WheelFilename,
207        hashes: &[HashDigest],
208        tags: Option<&Tags>,
209        hasher: &HashStrategy,
210        build_options: &BuildOptions,
211    ) -> WheelCompatibility {
212        // Check if binaries are allowed for this package.
213        if build_options.no_binary_package(&filename.name) {
214            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
215        }
216
217        // Determine a compatibility for the wheel based on tags.
218        let priority = match tags {
219            Some(tags) => match filename.compatibility(tags) {
220                TagCompatibility::Incompatible(tag) => {
221                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
222                }
223                TagCompatibility::Compatible(priority) => Some(priority),
224            },
225            None => None,
226        };
227
228        // Check if hashes line up.
229        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
230        let hash = if hash_policy.requires_validation() {
231            if hashes.is_empty() {
232                HashComparison::Missing
233            } else if hash_policy.matches(hashes) {
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}