mirror of https://github.com/astral-sh/uv
261 lines
9.5 KiB
Rust
261 lines
9.5 KiB
Rust
use std::borrow::Cow;
|
|
use uv_cache::{Cache, CacheBucket, CacheShard, WheelCache};
|
|
use uv_cache_info::CacheInfo;
|
|
use uv_cache_key::cache_digest;
|
|
use uv_configuration::{ConfigSettings, PackageConfigSettings};
|
|
use uv_distribution_types::{
|
|
DirectUrlSourceDist, DirectorySourceDist, GitSourceDist, Hashed, PathSourceDist,
|
|
};
|
|
use uv_normalize::PackageName;
|
|
use uv_platform_tags::Tags;
|
|
use uv_types::HashStrategy;
|
|
|
|
use crate::Error;
|
|
use crate::index::cached_wheel::CachedWheel;
|
|
use crate::source::{HTTP_REVISION, HttpRevisionPointer, LOCAL_REVISION, LocalRevisionPointer};
|
|
|
|
/// A local index of built distributions for a specific source distribution.
|
|
#[derive(Debug)]
|
|
pub struct BuiltWheelIndex<'a> {
|
|
cache: &'a Cache,
|
|
tags: &'a Tags,
|
|
hasher: &'a HashStrategy,
|
|
config_settings: &'a ConfigSettings,
|
|
config_settings_package: &'a PackageConfigSettings,
|
|
}
|
|
|
|
impl<'a> BuiltWheelIndex<'a> {
|
|
/// Initialize an index of built distributions.
|
|
pub fn new(
|
|
cache: &'a Cache,
|
|
tags: &'a Tags,
|
|
hasher: &'a HashStrategy,
|
|
config_settings: &'a ConfigSettings,
|
|
config_settings_package: &'a PackageConfigSettings,
|
|
) -> Self {
|
|
Self {
|
|
cache,
|
|
tags,
|
|
hasher,
|
|
config_settings,
|
|
config_settings_package,
|
|
}
|
|
}
|
|
|
|
/// Return the most compatible [`CachedWheel`] for a given source distribution at a direct URL.
|
|
///
|
|
/// This method does not perform any freshness checks and assumes that the source distribution
|
|
/// is already up-to-date.
|
|
pub fn url(&self, source_dist: &DirectUrlSourceDist) -> Result<Option<CachedWheel>, Error> {
|
|
// For direct URLs, cache directly under the hash of the URL itself.
|
|
let cache_shard = self.cache.shard(
|
|
CacheBucket::SourceDistributions,
|
|
WheelCache::Url(source_dist.url.raw()).root(),
|
|
);
|
|
|
|
// Read the revision from the cache.
|
|
let Some(pointer) = HttpRevisionPointer::read_from(cache_shard.entry(HTTP_REVISION))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
|
|
// Enforce hash-checking by omitting any wheels that don't satisfy the required hashes.
|
|
let revision = pointer.into_revision();
|
|
if !revision.satisfies(self.hasher.get(source_dist)) {
|
|
return Ok(None);
|
|
}
|
|
|
|
let cache_shard = cache_shard.shard(revision.id());
|
|
|
|
// If there are build settings, we need to scope to a cache shard.
|
|
let config_settings = self.config_settings_for(&source_dist.name);
|
|
let cache_shard = if config_settings.is_empty() {
|
|
cache_shard
|
|
} else {
|
|
cache_shard.shard(cache_digest(&config_settings))
|
|
};
|
|
|
|
Ok(self.find(&cache_shard))
|
|
}
|
|
/// Return the most compatible [`CachedWheel`] for a given source distribution at a local path.
|
|
pub fn path(&self, source_dist: &PathSourceDist) -> Result<Option<CachedWheel>, Error> {
|
|
let cache_shard = self.cache.shard(
|
|
CacheBucket::SourceDistributions,
|
|
WheelCache::Path(&source_dist.url).root(),
|
|
);
|
|
|
|
// Read the revision from the cache.
|
|
let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
|
|
// If the distribution is stale, omit it from the index.
|
|
let cache_info =
|
|
CacheInfo::from_file(&source_dist.install_path).map_err(Error::CacheRead)?;
|
|
if cache_info != *pointer.cache_info() {
|
|
return Ok(None);
|
|
}
|
|
|
|
// Enforce hash-checking by omitting any wheels that don't satisfy the required hashes.
|
|
let revision = pointer.into_revision();
|
|
if !revision.satisfies(self.hasher.get(source_dist)) {
|
|
return Ok(None);
|
|
}
|
|
|
|
let cache_shard = cache_shard.shard(revision.id());
|
|
|
|
// If there are build settings, we need to scope to a cache shard.
|
|
let config_settings = self.config_settings_for(&source_dist.name);
|
|
let cache_shard = if config_settings.is_empty() {
|
|
cache_shard
|
|
} else {
|
|
cache_shard.shard(cache_digest(&config_settings))
|
|
};
|
|
|
|
Ok(self
|
|
.find(&cache_shard)
|
|
.map(|wheel| wheel.with_cache_info(cache_info)))
|
|
}
|
|
|
|
/// Return the most compatible [`CachedWheel`] for a given source distribution built from a
|
|
/// local directory (source tree).
|
|
pub fn directory(
|
|
&self,
|
|
source_dist: &DirectorySourceDist,
|
|
) -> Result<Option<CachedWheel>, Error> {
|
|
let cache_shard = self.cache.shard(
|
|
CacheBucket::SourceDistributions,
|
|
if source_dist.editable.unwrap_or(false) {
|
|
WheelCache::Editable(&source_dist.url).root()
|
|
} else {
|
|
WheelCache::Path(&source_dist.url).root()
|
|
},
|
|
);
|
|
|
|
// Read the revision from the cache.
|
|
let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
|
|
// If the distribution is stale, omit it from the index.
|
|
let cache_info = CacheInfo::from_directory(&source_dist.install_path)?;
|
|
|
|
if cache_info != *pointer.cache_info() {
|
|
return Ok(None);
|
|
}
|
|
|
|
// Enforce hash-checking by omitting any wheels that don't satisfy the required hashes.
|
|
let revision = pointer.into_revision();
|
|
if !revision.satisfies(self.hasher.get(source_dist)) {
|
|
return Ok(None);
|
|
}
|
|
|
|
let cache_shard = cache_shard.shard(revision.id());
|
|
|
|
// If there are build settings, we need to scope to a cache shard.
|
|
let config_settings = self.config_settings_for(&source_dist.name);
|
|
let cache_shard = if config_settings.is_empty() {
|
|
cache_shard
|
|
} else {
|
|
cache_shard.shard(cache_digest(&config_settings))
|
|
};
|
|
|
|
Ok(self
|
|
.find(&cache_shard)
|
|
.map(|wheel| wheel.with_cache_info(cache_info)))
|
|
}
|
|
|
|
/// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL.
|
|
pub fn git(&self, source_dist: &GitSourceDist) -> Option<CachedWheel> {
|
|
// Enforce hash-checking, which isn't supported for Git distributions.
|
|
if self.hasher.get(source_dist).is_validate() {
|
|
return None;
|
|
}
|
|
|
|
let git_sha = source_dist.git.precise()?;
|
|
|
|
let cache_shard = self.cache.shard(
|
|
CacheBucket::SourceDistributions,
|
|
WheelCache::Git(&source_dist.url, git_sha.as_short_str()).root(),
|
|
);
|
|
|
|
// If there are build settings, we need to scope to a cache shard.
|
|
let config_settings = self.config_settings_for(&source_dist.name);
|
|
let cache_shard = if config_settings.is_empty() {
|
|
cache_shard
|
|
} else {
|
|
cache_shard.shard(cache_digest(&config_settings))
|
|
};
|
|
|
|
self.find(&cache_shard)
|
|
}
|
|
|
|
/// Find the "best" distribution in the index for a given source distribution.
|
|
///
|
|
/// This lookup prefers newer versions over older versions, and aims to maximize compatibility
|
|
/// with the target platform.
|
|
///
|
|
/// The `shard` should point to a directory containing the built distributions for a specific
|
|
/// source distribution. For example, given the built wheel cache structure:
|
|
/// ```text
|
|
/// built-wheels-v0/
|
|
/// └── pypi
|
|
/// └── django-allauth-0.51.0.tar.gz
|
|
/// ├── django_allauth-0.51.0-py3-none-any.whl
|
|
/// └── metadata.json
|
|
/// ```
|
|
///
|
|
/// The `shard` should be `built-wheels-v0/pypi/django-allauth-0.51.0.tar.gz`.
|
|
fn find(&self, shard: &CacheShard) -> Option<CachedWheel> {
|
|
let mut candidate: Option<CachedWheel> = None;
|
|
|
|
// Unzipped wheels are stored as symlinks into the archive directory.
|
|
for wheel_dir in uv_fs::entries(shard).ok().into_iter().flatten() {
|
|
// Ignore any `.lock` files.
|
|
if wheel_dir
|
|
.extension()
|
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
match CachedWheel::from_built_source(&wheel_dir, self.cache) {
|
|
None => {}
|
|
Some(dist_info) => {
|
|
// Pick the wheel with the highest priority
|
|
let compatibility = dist_info.filename.compatibility(self.tags);
|
|
|
|
// Only consider wheels that are compatible with our tags.
|
|
if !compatibility.is_compatible() {
|
|
continue;
|
|
}
|
|
|
|
if let Some(existing) = candidate.as_ref() {
|
|
// Override if the wheel is newer, or "more" compatible.
|
|
if dist_info.filename.version > existing.filename.version
|
|
|| compatibility > existing.filename.compatibility(self.tags)
|
|
{
|
|
candidate = Some(dist_info);
|
|
}
|
|
} else {
|
|
candidate = Some(dist_info);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
candidate
|
|
}
|
|
|
|
/// Determine the [`ConfigSettings`] for the given package name.
|
|
fn config_settings_for(&self, name: &PackageName) -> Cow<'_, ConfigSettings> {
|
|
if let Some(package_settings) = self.config_settings_package.get(name) {
|
|
Cow::Owned(package_settings.clone().merge(self.config_settings.clone()))
|
|
} else {
|
|
Cow::Borrowed(self.config_settings)
|
|
}
|
|
}
|
|
}
|