Skip to main content

uv_resolver/
flat_index.rs

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