Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow dependency metadata entries for direct URL requirements #7846

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 50 additions & 14 deletions crates/uv-distribution-types/src/dependency_metadata.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::Requirement;
Expand All @@ -20,22 +21,57 @@ impl DependencyMetadata {
}

/// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`].
pub fn get(&self, package: &PackageName, version: &Version) -> Option<ResolutionMetadata> {
pub fn get(
&self,
package: &PackageName,
version: Option<&Version>,
) -> Option<ResolutionMetadata> {
let versions = self.0.get(package)?;

// Search for an exact, then a global match.
let metadata = versions
.iter()
.find(|v| v.version.as_ref() == Some(version))
.or_else(|| versions.iter().find(|v| v.version.is_none()))?;

Some(ResolutionMetadata {
name: metadata.name.clone(),
version: version.clone(),
requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(),
})
if let Some(version) = version {
// If a specific version was requested, search for an exact match, then a global match.
let metadata = versions
.iter()
.find(|v| v.version.as_ref() == Some(version))
.inspect(|_| {
debug!("Found dependency metadata entry for `{package}=={version}`",);
})
.or_else(|| versions.iter().find(|v| v.version.is_none()))
.inspect(|_| {
debug!("Found global metadata entry for `{package}`",);
});
let Some(metadata) = metadata else {
warn!("No dependency metadata entry found for `{package}=={version}`");
return None;
};
debug!("Found dependency metadata entry for `{package}=={version}`",);
Some(ResolutionMetadata {
name: metadata.name.clone(),
version: version.clone(),
requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(),
})
} else {
// If no version was requested (i.e., it's a direct URL dependency), allow a single
// versioned match.
let [metadata] = versions.as_slice() else {
warn!("Multiple dependency metadata entries found for `{package}`");
return None;
};
let Some(version) = metadata.version.clone() else {
warn!("No version found in dependency metadata entry for `{package}`");
return None;
};
debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)");
Some(ResolutionMetadata {
name: metadata.name.clone(),
version,
requires_dist: metadata.requires_dist.clone(),
requires_python: metadata.requires_python.clone(),
provides_extras: metadata.provides_extras.clone(),
})
}
}

/// Retrieve all [`StaticMetadata`] entries.
Expand Down
22 changes: 12 additions & 10 deletions crates/uv-distribution/src/distribution_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
///
/// While hashes will be generated in some cases, hash-checking is _not_ enforced and should
/// instead be enforced by the caller.
pub async fn get_wheel_metadata(
async fn get_wheel_metadata(
&self,
dist: &BuiltDist,
hashes: HashPolicy<'_>,
Expand All @@ -363,7 +363,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
if let Some(metadata) = self
.build_context
.dependency_metadata()
.get(dist.name(), dist.version())
.get(dist.name(), Some(dist.version()))
{
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
}
Expand Down Expand Up @@ -425,14 +425,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
) -> Result<ArchiveMetadata, Error> {
// If the metadata was provided by the user directly, prefer it.
if let Some(dist) = source.as_dist() {
if let Some(version) = dist.version() {
if let Some(metadata) = self
.build_context
.dependency_metadata()
.get(dist.name(), version)
{
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
}
if let Some(metadata) = self
.build_context
.dependency_metadata()
.get(dist.name(), dist.version())
{
// If we skipped the build, we should still resolve any Git dependencies to precise
// commits.
self.builder.resolve_revision(source, &self.client).await?;

return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
}
}

Expand Down
34 changes: 34 additions & 0 deletions crates/uv-distribution/src/source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,40 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
))
}

/// Resolve a source to a specific revision.
pub(crate) async fn resolve_revision(
&self,
source: &BuildableSource<'_>,
client: &ManagedClient<'_>,
) -> Result<(), Error> {
match source {
BuildableSource::Dist(SourceDist::Git(source)) => {
self.build_context
.git()
.fetch(
&source.git,
client.unmanaged.uncached_client(&source.url).clone(),
self.build_context.cache().bucket(CacheBucket::Git),
self.reporter.clone().map(Facade::from),
)
.await?;
}
BuildableSource::Url(SourceUrl::Git(source)) => {
self.build_context
.git()
.fetch(
source.git,
client.unmanaged.uncached_client(source.url).clone(),
self.build_context.cache().bucket(CacheBucket::Git),
self.reporter.clone().map(Facade::from),
)
.await?;
}
_ => {}
}
Ok(())
}

/// Heal a [`Revision`] for a local archive.
async fn heal_archive_revision(
&self,
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-git/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ pub(crate) fn fetch(
}
}

/// Attempts to use `git` CLI installed on the system to fetch a repository,.
/// Attempts to use `git` CLI installed on the system to fetch a repository.
fn fetch_with_cli(
repo: &mut GitRepository,
url: &str,
Expand Down
44 changes: 44 additions & 0 deletions crates/uv-git/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,50 @@ impl GitResolver {
self.0.get(reference)
}

/// Resolve a Git URL to a specific commit.
pub async fn resolve(
&self,
url: &GitUrl,
client: ClientWithMiddleware,
cache: PathBuf,
reporter: Option<impl Reporter + 'static>,
) -> Result<GitSha, GitResolverError> {
debug!("Resolving source distribution from Git: {url}");

let reference = RepositoryReference::from(url);

// If we know the precise commit already, return it.
if let Some(precise) = self.get(&reference) {
return Ok(*precise);
}

// Avoid races between different processes, too.
let lock_dir = cache.join("locks");
fs::create_dir_all(&lock_dir).await?;
let repository_url = RepositoryUrl::new(url.repository());
let _lock = LockedFile::acquire(
lock_dir.join(cache_digest(&repository_url)),
&repository_url,
)
.await?;

// Fetch the Git repository.
let source = if let Some(reporter) = reporter {
GitSource::new(url.clone(), client, cache).with_reporter(reporter)
} else {
GitSource::new(url.clone(), client, cache)
};
let precise = tokio::task::spawn_blocking(move || source.resolve())
.await?
.map_err(GitResolverError::Git)?;

// Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches
// resolve to the same precise commit.
self.insert(reference, precise);

Ok(precise)
}

/// Fetch a remote Git repository.
pub async fn fetch(
&self,
Expand Down
62 changes: 62 additions & 0 deletions crates/uv-git/src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,68 @@ impl GitSource {
}
}

/// Resolve a Git source to a specific revision.
#[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))]
pub fn resolve(self) -> Result<GitSha> {
// Compute the canonical URL for the repository.
let canonical = RepositoryUrl::new(&self.git.repository);

// The path to the repo, within the Git database.
let ident = cache_digest(&canonical);
let db_path = self.cache.join("db").join(&ident);

// Authenticate the URL, if necessary.
let remote = if let Some(credentials) = GIT_STORE.get(&canonical) {
Cow::Owned(credentials.apply(self.git.repository.clone()))
} else {
Cow::Borrowed(&self.git.repository)
};

let remote = GitRemote::new(&remote);
let (db, actual_rev, task) = match (self.git.precise, remote.db_at(&db_path).ok()) {
// If we have a locked revision, and we have a preexisting database
// which has that revision, then no update needs to happen.
(Some(rev), Some(db)) if db.contains(rev.into()) => {
debug!("Using existing Git source `{}`", self.git.repository);
(db, rev, None)
}

// ... otherwise we use this state to update the git database. Note
// that we still check for being offline here, for example in the
// situation that we have a locked revision but the database
// doesn't have it.
(locked_rev, db) => {
debug!("Updating Git source `{}`", self.git.repository);

// Report the checkout operation to the reporter.
let task = self.reporter.as_ref().map(|reporter| {
reporter.on_checkout_start(remote.url(), self.git.reference.as_rev())
});

let (db, actual_rev) = remote.checkout(
&db_path,
db,
&self.git.reference,
locked_rev.map(GitOid::from),
&self.client,
)?;

(db, GitSha::from(actual_rev), task)
}
};

let short_id = db.to_short_id(actual_rev.into())?;

// Report the checkout operation to the reporter.
if let Some(task) = task {
if let Some(reporter) = self.reporter.as_ref() {
reporter.on_checkout_complete(remote.url(), short_id.as_str(), task);
}
}

Ok(actual_rev)
}

/// Fetch the underlying Git repository at the given revision.
#[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))]
pub fn fetch(self) -> Result<Fetch> {
Expand Down
4 changes: 1 addition & 3 deletions crates/uv-resolver/src/redirect.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use url::Url;

use uv_git::{GitReference, GitResolver};
use uv_pep508::VerbatimUrl;
use uv_pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl};
Expand All @@ -17,9 +16,8 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba
let Some(new_git_url) = git.precise(git_url.clone()) else {
debug_assert!(
matches!(git_url.reference(), GitReference::FullCommit(_)),
"Unseen Git URL: {}, {:?}",
"Unseen Git URL: {}, {git_url:?}",
url.verbatim,
git_url
);
return url;
};
Expand Down
Loading
Loading