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    PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, SourceDistCompatibility,
15    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    ) -> Self {
89        let mut distributions = Self::default();
90        for entry in entries {
91            let (filename, file, index) = entry.into_parts();
92            distributions.add_file(file, filename, tags, hasher, build_options, index);
93        }
94        distributions
95    }
96
97    /// Returns an [`Iterator`] over the distributions.
98    pub(crate) fn iter(&self) -> impl Iterator<Item = (&Version, &PrioritizedDist)> {
99        self.0.iter()
100    }
101
102    /// Add the given [`File`] to the [`FlatDistributions`] for the given package.
103    fn add_file(
104        &mut self,
105        file: File,
106        filename: DistFilename,
107        tags: Option<&Tags>,
108        hasher: &HashStrategy,
109        build_options: &BuildOptions,
110        index: IndexUrl,
111    ) {
112        // No `requires-python` here: for source distributions, we don't have that information;
113        // for wheels, we read it lazily only when selected.
114        match filename {
115            DistFilename::WheelFilename(filename) => {
116                let version = filename.version.clone();
117
118                let compatibility = Self::wheel_compatibility(
119                    &filename,
120                    file.hashes.as_slice(),
121                    tags,
122                    hasher,
123                    build_options,
124                );
125                let dist = RegistryBuiltWheel {
126                    filename,
127                    file: Box::new(file),
128                    index,
129                    size_is_authoritative: false,
130                };
131                self.0
132                    .entry(version)
133                    .or_default()
134                    .insert_built(dist, vec![], compatibility);
135            }
136            DistFilename::SourceDistFilename(filename) => {
137                let compatibility = Self::source_dist_compatibility(
138                    &filename,
139                    file.hashes.as_slice(),
140                    hasher,
141                    build_options,
142                );
143                let dist = RegistrySourceDist {
144                    name: filename.name.clone(),
145                    version: filename.version.clone(),
146                    ext: filename.extension,
147                    file: Box::new(file),
148                    index,
149                    wheels: vec![],
150                    size_is_authoritative: false,
151                };
152                self.0.entry(filename.version).or_default().insert_source(
153                    dist,
154                    vec![],
155                    compatibility,
156                );
157            }
158        }
159    }
160
161    fn source_dist_compatibility(
162        filename: &SourceDistFilename,
163        hashes: &[HashDigest],
164        hasher: &HashStrategy,
165        build_options: &BuildOptions,
166    ) -> SourceDistCompatibility {
167        // Check if source distributions are allowed for this package.
168        if build_options.no_build_package(&filename.name) {
169            return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild);
170        }
171
172        // Check if the filename is PEP 625-compliant.
173        // TODO: Strengthen this check more; right now we allow `.zip`
174        // (which is not compliant) and we don't strictly
175        // enforce the formatting rules for the name or version.
176        if !filename.extension.is_pep625_compliant() {
177            return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename);
178        }
179
180        // Check if hashes line up
181        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
182        let hash = if hash_policy.requires_validation() {
183            if hashes.is_empty() {
184                HashComparison::Missing
185            } else if hash_policy.matches(hashes) {
186                HashComparison::Matched
187            } else {
188                HashComparison::Mismatched
189            }
190        } else {
191            HashComparison::Matched
192        };
193
194        SourceDistCompatibility::Compatible(hash)
195    }
196
197    fn wheel_compatibility(
198        filename: &WheelFilename,
199        hashes: &[HashDigest],
200        tags: Option<&Tags>,
201        hasher: &HashStrategy,
202        build_options: &BuildOptions,
203    ) -> WheelCompatibility {
204        // Check if binaries are allowed for this package.
205        if build_options.no_binary_package(&filename.name) {
206            return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary);
207        }
208
209        // Determine a compatibility for the wheel based on tags.
210        let priority = match tags {
211            Some(tags) => match filename.compatibility(tags) {
212                TagCompatibility::Incompatible(tag) => {
213                    return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag));
214                }
215                TagCompatibility::Compatible(priority) => Some(priority),
216            },
217            None => None,
218        };
219
220        // Check if hashes line up.
221        let hash_policy = hasher.archive_policy_for_package(&filename.name, &filename.version);
222        let hash = if hash_policy.requires_validation() {
223            if hashes.is_empty() {
224                HashComparison::Missing
225            } else if hash_policy.matches(hashes) {
226                HashComparison::Matched
227            } else {
228                HashComparison::Mismatched
229            }
230        } else {
231            HashComparison::Matched
232        };
233
234        // Break ties with the build tag.
235        let build_tag = filename.build_tag().cloned();
236
237        WheelCompatibility::Compatible(hash, priority, build_tag)
238    }
239}
240
241impl IntoIterator for FlatDistributions {
242    type Item = (Version, PrioritizedDist);
243    type IntoIter = std::collections::btree_map::IntoIter<Version, PrioritizedDist>;
244
245    fn into_iter(self) -> Self::IntoIter {
246        self.0.into_iter()
247    }
248}
249
250impl From<FlatDistributions> for BTreeMap<Version, PrioritizedDist> {
251    fn from(distributions: FlatDistributions) -> Self {
252        distributions.0
253    }
254}