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