uv_distribution/index/
registry_wheel_index.rs

1use std::borrow::Cow;
2use std::collections::hash_map::Entry;
3
4use rustc_hash::{FxHashMap, FxHashSet};
5
6use uv_cache::{Cache, CacheBucket, WheelCache};
7use uv_cache_info::CacheInfo;
8use uv_distribution_types::{
9    BuildInfo, BuildVariables, CachedRegistryDist, ConfigSettings, ExtraBuildRequirement,
10    ExtraBuildRequires, ExtraBuildVariables, Hashed, Index, IndexLocations, IndexUrl,
11    PackageConfigSettings,
12};
13use uv_fs::{directories, files};
14use uv_normalize::PackageName;
15use uv_platform_tags::Tags;
16use uv_types::HashStrategy;
17
18use crate::index::cached_wheel::{CachedWheel, ResolvedWheel};
19use crate::source::{HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer};
20
21/// An entry in the [`RegistryWheelIndex`].
22#[derive(Debug, Clone, Hash, PartialEq, Eq)]
23pub struct IndexEntry<'index> {
24    /// The cached distribution.
25    pub dist: CachedRegistryDist,
26    /// Whether the wheel was built from source (true), or downloaded from the registry directly (false).
27    pub built: bool,
28    /// The index from which the wheel was downloaded.
29    pub index: &'index Index,
30}
31
32/// A local index of distributions that originate from a registry, like `PyPI`.
33#[derive(Debug)]
34pub struct RegistryWheelIndex<'a> {
35    cache: &'a Cache,
36    tags: &'a Tags,
37    index_locations: &'a IndexLocations,
38    hasher: &'a HashStrategy,
39    index: FxHashMap<&'a PackageName, Vec<IndexEntry<'a>>>,
40    config_settings: &'a ConfigSettings,
41    config_settings_package: &'a PackageConfigSettings,
42    extra_build_requires: &'a ExtraBuildRequires,
43    extra_build_variables: &'a ExtraBuildVariables,
44}
45
46impl<'a> RegistryWheelIndex<'a> {
47    /// Initialize an index of registry distributions.
48    pub fn new(
49        cache: &'a Cache,
50        tags: &'a Tags,
51        index_locations: &'a IndexLocations,
52        hasher: &'a HashStrategy,
53        config_settings: &'a ConfigSettings,
54        config_settings_package: &'a PackageConfigSettings,
55        extra_build_requires: &'a ExtraBuildRequires,
56        extra_build_variables: &'a ExtraBuildVariables,
57    ) -> Self {
58        Self {
59            cache,
60            tags,
61            index_locations,
62            hasher,
63            config_settings,
64            config_settings_package,
65            extra_build_requires,
66            extra_build_variables,
67            index: FxHashMap::default(),
68        }
69    }
70
71    /// Return an iterator over available wheels for a given package.
72    ///
73    /// If the package is not yet indexed, this will index the package by reading from the cache.
74    pub fn get(&mut self, name: &'a PackageName) -> impl Iterator<Item = &IndexEntry<'_>> {
75        self.get_impl(name).iter().rev()
76    }
77
78    /// Get an entry in the index.
79    fn get_impl(&mut self, name: &'a PackageName) -> &[IndexEntry<'_>] {
80        (match self.index.entry(name) {
81            Entry::Occupied(entry) => entry.into_mut(),
82            Entry::Vacant(entry) => entry.insert(Self::index(
83                name,
84                self.cache,
85                self.tags,
86                self.index_locations,
87                self.hasher,
88                self.config_settings,
89                self.config_settings_package,
90                self.extra_build_requires,
91                self.extra_build_variables,
92            )),
93        }) as _
94    }
95
96    /// Add a package to the index by reading from the cache.
97    fn index<'index>(
98        package: &PackageName,
99        cache: &Cache,
100        tags: &Tags,
101        index_locations: &'index IndexLocations,
102        hasher: &HashStrategy,
103        config_settings: &ConfigSettings,
104        config_settings_package: &PackageConfigSettings,
105        extra_build_requires: &ExtraBuildRequires,
106        extra_build_variables: &ExtraBuildVariables,
107    ) -> Vec<IndexEntry<'index>> {
108        let mut entries = vec![];
109
110        let mut seen = FxHashSet::default();
111        for index in index_locations.allowed_indexes() {
112            if !seen.insert(index.url()) {
113                continue;
114            }
115
116            // Index all the wheels that were downloaded directly from the registry.
117            let wheel_dir = cache.shard(
118                CacheBucket::Wheels,
119                WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
120            );
121
122            // For registry wheels, the cache structure is: `<index>/<package-name>/<wheel>.http`
123            // or `<index>/<package-name>/<version>/<wheel>.rev`.
124            for file in files(&wheel_dir).ok().into_iter().flatten() {
125                match index.url() {
126                    // Add files from remote registries.
127                    IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
128                        if file
129                            .extension()
130                            .is_some_and(|ext| ext.eq_ignore_ascii_case("http"))
131                        {
132                            if let Some(wheel) =
133                                CachedWheel::from_http_pointer(wheel_dir.join(file), cache)
134                            {
135                                if wheel.filename.compatibility(tags).is_compatible() {
136                                    // Enforce hash-checking based on the built distribution.
137                                    if wheel.satisfies(
138                                        hasher.get_package(
139                                            &wheel.filename.name,
140                                            &wheel.filename.version,
141                                        ),
142                                    ) {
143                                        entries.push(IndexEntry {
144                                            dist: wheel.into_registry_dist(),
145                                            index,
146                                            built: false,
147                                        });
148                                    }
149                                }
150                            }
151                        }
152                    }
153                    // Add files from local registries (e.g., `--find-links`).
154                    IndexUrl::Path(_) => {
155                        if file
156                            .extension()
157                            .is_some_and(|ext| ext.eq_ignore_ascii_case("rev"))
158                        {
159                            if let Some(wheel) =
160                                CachedWheel::from_local_pointer(wheel_dir.join(file), cache)
161                            {
162                                if wheel.filename.compatibility(tags).is_compatible() {
163                                    // Enforce hash-checking based on the built distribution.
164                                    if wheel.satisfies(
165                                        hasher.get_package(
166                                            &wheel.filename.name,
167                                            &wheel.filename.version,
168                                        ),
169                                    ) {
170                                        entries.push(IndexEntry {
171                                            dist: wheel.into_registry_dist(),
172                                            index,
173                                            built: false,
174                                        });
175                                    }
176                                }
177                            }
178                        }
179                    }
180                }
181            }
182
183            // Index all the built wheels, created by downloading and building source distributions
184            // from the registry.
185            let cache_shard = cache.shard(
186                CacheBucket::SourceDistributions,
187                WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
188            );
189
190            // For registry source distributions, the cache structure is: `<index>/<package-name>/<version>/`.
191            for shard in directories(&cache_shard).ok().into_iter().flatten() {
192                let cache_shard = cache_shard.shard(shard);
193
194                // Read the revision from the cache.
195                let revision = match index.url() {
196                    // Add files from remote registries.
197                    IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
198                        let revision_entry = cache_shard.entry(HTTP_REVISION);
199                        if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision_entry) {
200                            Some(pointer.into_revision())
201                        } else {
202                            None
203                        }
204                    }
205                    // Add files from local registries (e.g., `--find-links`).
206                    IndexUrl::Path(_) => {
207                        let revision_entry = cache_shard.entry(LOCAL_REVISION);
208                        if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision_entry) {
209                            Some(pointer.into_revision())
210                        } else {
211                            None
212                        }
213                    }
214                };
215
216                if let Some(revision) = revision {
217                    let cache_shard = cache_shard.shard(revision.id());
218
219                    // If there are build settings, we need to scope to a cache shard.
220                    let extra_build_deps =
221                        Self::extra_build_requires_for(package, extra_build_requires);
222                    let extra_build_vars =
223                        Self::extra_build_variables_for(package, extra_build_variables);
224                    let config_settings = Self::config_settings_for(
225                        package,
226                        config_settings,
227                        config_settings_package,
228                    );
229                    let build_info = BuildInfo::from_settings(
230                        &config_settings,
231                        extra_build_deps,
232                        extra_build_vars,
233                    );
234                    let cache_shard = build_info
235                        .cache_shard()
236                        .map(|digest| cache_shard.shard(digest))
237                        .unwrap_or(cache_shard);
238
239                    for wheel_dir in uv_fs::entries(cache_shard).ok().into_iter().flatten() {
240                        // Ignore any `.lock` files.
241                        if wheel_dir
242                            .extension()
243                            .is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
244                        {
245                            continue;
246                        }
247
248                        if let Some(wheel) = ResolvedWheel::from_built_source(wheel_dir, cache) {
249                            if wheel.filename.compatibility(tags).is_compatible() {
250                                // Enforce hash-checking based on the source distribution.
251                                if revision.satisfies(
252                                    hasher
253                                        .get_package(&wheel.filename.name, &wheel.filename.version),
254                                ) {
255                                    let wheel = CachedWheel::from_entry(
256                                        wheel,
257                                        revision.hashes().into(),
258                                        CacheInfo::default(),
259                                        build_info.clone(),
260                                    );
261                                    entries.push(IndexEntry {
262                                        dist: wheel.into_registry_dist(),
263                                        index,
264                                        built: true,
265                                    });
266                                }
267                            }
268                        }
269                    }
270                }
271            }
272        }
273
274        // Sort the cached distributions by (1) version, (2) compatibility, and (3) build status.
275        // We want the highest versions, with the greatest compatibility, that were built from source.
276        // at the end of the list.
277        entries.sort_unstable_by(|a, b| {
278            a.dist
279                .filename
280                .version
281                .cmp(&b.dist.filename.version)
282                .then_with(|| {
283                    a.dist
284                        .filename
285                        .compatibility(tags)
286                        .cmp(&b.dist.filename.compatibility(tags))
287                        .then_with(|| a.built.cmp(&b.built))
288                })
289        });
290
291        entries
292    }
293
294    /// Determine the [`ConfigSettings`] for the given package name.
295    fn config_settings_for<'settings>(
296        name: &PackageName,
297        config_settings: &'settings ConfigSettings,
298        config_settings_package: &PackageConfigSettings,
299    ) -> Cow<'settings, ConfigSettings> {
300        if let Some(package_settings) = config_settings_package.get(name) {
301            Cow::Owned(package_settings.clone().merge(config_settings.clone()))
302        } else {
303            Cow::Borrowed(config_settings)
304        }
305    }
306
307    /// Determine the extra build requirements for the given package name.
308    fn extra_build_requires_for<'settings>(
309        name: &PackageName,
310        extra_build_requires: &'settings ExtraBuildRequires,
311    ) -> &'settings [ExtraBuildRequirement] {
312        extra_build_requires
313            .get(name)
314            .map(Vec::as_slice)
315            .unwrap_or(&[])
316    }
317
318    /// Determine the extra build variables for the given package name.
319    fn extra_build_variables_for<'settings>(
320        name: &PackageName,
321        extra_build_variables: &'settings ExtraBuildVariables,
322    ) -> Option<&'settings BuildVariables> {
323        extra_build_variables.get(name)
324    }
325}