Skip to content

Commit 221a2b3

Browse files
committed
Allow dependency metadata entires for direct URL requirements
1 parent 21b9254 commit 221a2b3

File tree

10 files changed

+341
-28
lines changed

10 files changed

+341
-28
lines changed

crates/uv-distribution-types/src/dependency_metadata.rs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use rustc_hash::FxHashMap;
22
use serde::{Deserialize, Serialize};
3+
use tracing::{debug, warn};
34
use uv_normalize::{ExtraName, PackageName};
45
use uv_pep440::{Version, VersionSpecifiers};
56
use uv_pep508::Requirement;
@@ -20,22 +21,57 @@ impl DependencyMetadata {
2021
}
2122

2223
/// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`].
23-
pub fn get(&self, package: &PackageName, version: &Version) -> Option<ResolutionMetadata> {
24+
pub fn get(
25+
&self,
26+
package: &PackageName,
27+
version: Option<&Version>,
28+
) -> Option<ResolutionMetadata> {
2429
let versions = self.0.get(package)?;
2530

26-
// Search for an exact, then a global match.
27-
let metadata = versions
28-
.iter()
29-
.find(|v| v.version.as_ref() == Some(version))
30-
.or_else(|| versions.iter().find(|v| v.version.is_none()))?;
31-
32-
Some(ResolutionMetadata {
33-
name: metadata.name.clone(),
34-
version: version.clone(),
35-
requires_dist: metadata.requires_dist.clone(),
36-
requires_python: metadata.requires_python.clone(),
37-
provides_extras: metadata.provides_extras.clone(),
38-
})
31+
if let Some(version) = version {
32+
// If a specific version was requested, search for an exact match, then a global match.
33+
let metadata = versions
34+
.iter()
35+
.find(|v| v.version.as_ref() == Some(version))
36+
.inspect(|_| {
37+
debug!("Found dependency metadata entry for `{package}=={version}`",);
38+
})
39+
.or_else(|| versions.iter().find(|v| v.version.is_none()))
40+
.inspect(|_| {
41+
debug!("Found global metadata entry for `{package}`",);
42+
});
43+
let Some(metadata) = metadata else {
44+
warn!("No dependency metadata entry found for `{package}=={version}`");
45+
return None;
46+
};
47+
debug!("Found dependency metadata entry for `{package}=={version}`",);
48+
Some(ResolutionMetadata {
49+
name: metadata.name.clone(),
50+
version: version.clone(),
51+
requires_dist: metadata.requires_dist.clone(),
52+
requires_python: metadata.requires_python.clone(),
53+
provides_extras: metadata.provides_extras.clone(),
54+
})
55+
} else {
56+
// If no version was requested (i.e., it's a direct URL dependency), allow a single
57+
// versioned match.
58+
let [metadata] = versions.as_slice() else {
59+
warn!("Multiple dependency metadata entries found for `{package}`");
60+
return None;
61+
};
62+
let Some(version) = metadata.version.clone() else {
63+
warn!("No version found in dependency metadata entry for `{package}`");
64+
return None;
65+
};
66+
debug!("Found dependency metadata entry for `{package}` (assuming: `{version}`)");
67+
Some(ResolutionMetadata {
68+
name: metadata.name.clone(),
69+
version,
70+
requires_dist: metadata.requires_dist.clone(),
71+
requires_python: metadata.requires_python.clone(),
72+
provides_extras: metadata.provides_extras.clone(),
73+
})
74+
}
3975
}
4076

4177
/// Retrieve all [`StaticMetadata`] entries.

crates/uv-distribution/src/distribution_database.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
354354
///
355355
/// While hashes will be generated in some cases, hash-checking is _not_ enforced and should
356356
/// instead be enforced by the caller.
357-
pub async fn get_wheel_metadata(
357+
async fn get_wheel_metadata(
358358
&self,
359359
dist: &BuiltDist,
360360
hashes: HashPolicy<'_>,
@@ -363,7 +363,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
363363
if let Some(metadata) = self
364364
.build_context
365365
.dependency_metadata()
366-
.get(dist.name(), dist.version())
366+
.get(dist.name(), Some(dist.version()))
367367
{
368368
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
369369
}
@@ -425,14 +425,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
425425
) -> Result<ArchiveMetadata, Error> {
426426
// If the metadata was provided by the user directly, prefer it.
427427
if let Some(dist) = source.as_dist() {
428-
if let Some(version) = dist.version() {
429-
if let Some(metadata) = self
430-
.build_context
431-
.dependency_metadata()
432-
.get(dist.name(), version)
433-
{
434-
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
435-
}
428+
if let Some(metadata) = self
429+
.build_context
430+
.dependency_metadata()
431+
.get(dist.name(), dist.version())
432+
{
433+
// If we skipped the build, we should still resolve any Git dependencies to precise
434+
// commits.
435+
self.builder.resolve_revision(source, &self.client).await?;
436+
437+
return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
436438
}
437439
}
438440

crates/uv-distribution/src/source/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,40 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
14971497
))
14981498
}
14991499

1500+
/// Resolve a source to a specific revision.
1501+
pub(crate) async fn resolve_revision(
1502+
&self,
1503+
source: &BuildableSource<'_>,
1504+
client: &ManagedClient<'_>,
1505+
) -> Result<(), Error> {
1506+
match source {
1507+
BuildableSource::Dist(SourceDist::Git(source)) => {
1508+
self.build_context
1509+
.git()
1510+
.fetch(
1511+
&source.git,
1512+
client.unmanaged.uncached_client(&source.url).clone(),
1513+
self.build_context.cache().bucket(CacheBucket::Git),
1514+
self.reporter.clone().map(Facade::from),
1515+
)
1516+
.await?;
1517+
}
1518+
BuildableSource::Url(SourceUrl::Git(source)) => {
1519+
self.build_context
1520+
.git()
1521+
.fetch(
1522+
source.git,
1523+
client.unmanaged.uncached_client(source.url).clone(),
1524+
self.build_context.cache().bucket(CacheBucket::Git),
1525+
self.reporter.clone().map(Facade::from),
1526+
)
1527+
.await?;
1528+
}
1529+
_ => {}
1530+
}
1531+
Ok(())
1532+
}
1533+
15001534
/// Heal a [`Revision`] for a local archive.
15011535
async fn heal_archive_revision(
15021536
&self,

crates/uv-git/src/git.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ pub(crate) fn fetch(
590590
}
591591
}
592592

593-
/// Attempts to use `git` CLI installed on the system to fetch a repository,.
593+
/// Attempts to use `git` CLI installed on the system to fetch a repository.
594594
fn fetch_with_cli(
595595
repo: &mut GitRepository,
596596
url: &str,

crates/uv-git/src/resolver.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,50 @@ impl GitResolver {
3838
self.0.get(reference)
3939
}
4040

41+
/// Resolve a Git URL to a specific commit.
42+
pub async fn resolve(
43+
&self,
44+
url: &GitUrl,
45+
client: ClientWithMiddleware,
46+
cache: PathBuf,
47+
reporter: Option<impl Reporter + 'static>,
48+
) -> Result<GitSha, GitResolverError> {
49+
debug!("Resolving source distribution from Git: {url}");
50+
51+
let reference = RepositoryReference::from(url);
52+
53+
// If we know the precise commit already, return it.
54+
if let Some(precise) = self.get(&reference) {
55+
return Ok(*precise);
56+
}
57+
58+
// Avoid races between different processes, too.
59+
let lock_dir = cache.join("locks");
60+
fs::create_dir_all(&lock_dir).await?;
61+
let repository_url = RepositoryUrl::new(url.repository());
62+
let _lock = LockedFile::acquire(
63+
lock_dir.join(cache_digest(&repository_url)),
64+
&repository_url,
65+
)
66+
.await?;
67+
68+
// Fetch the Git repository.
69+
let source = if let Some(reporter) = reporter {
70+
GitSource::new(url.clone(), client, cache).with_reporter(reporter)
71+
} else {
72+
GitSource::new(url.clone(), client, cache)
73+
};
74+
let precise = tokio::task::spawn_blocking(move || source.resolve())
75+
.await?
76+
.map_err(GitResolverError::Git)?;
77+
78+
// Insert the resolved URL into the in-memory cache. This ensures that subsequent fetches
79+
// resolve to the same precise commit.
80+
self.insert(reference, precise);
81+
82+
Ok(precise)
83+
}
84+
4185
/// Fetch a remote Git repository.
4286
pub async fn fetch(
4387
&self,

crates/uv-git/src/source.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,68 @@ impl GitSource {
5151
}
5252
}
5353

54+
/// Resolve a Git source to a specific revision.
55+
#[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))]
56+
pub fn resolve(self) -> Result<GitSha> {
57+
// Compute the canonical URL for the repository.
58+
let canonical = RepositoryUrl::new(&self.git.repository);
59+
60+
// The path to the repo, within the Git database.
61+
let ident = cache_digest(&canonical);
62+
let db_path = self.cache.join("db").join(&ident);
63+
64+
// Authenticate the URL, if necessary.
65+
let remote = if let Some(credentials) = GIT_STORE.get(&canonical) {
66+
Cow::Owned(credentials.apply(self.git.repository.clone()))
67+
} else {
68+
Cow::Borrowed(&self.git.repository)
69+
};
70+
71+
let remote = GitRemote::new(&remote);
72+
let (db, actual_rev, task) = match (self.git.precise, remote.db_at(&db_path).ok()) {
73+
// If we have a locked revision, and we have a preexisting database
74+
// which has that revision, then no update needs to happen.
75+
(Some(rev), Some(db)) if db.contains(rev.into()) => {
76+
debug!("Using existing Git source `{}`", self.git.repository);
77+
(db, rev, None)
78+
}
79+
80+
// ... otherwise we use this state to update the git database. Note
81+
// that we still check for being offline here, for example in the
82+
// situation that we have a locked revision but the database
83+
// doesn't have it.
84+
(locked_rev, db) => {
85+
debug!("Updating Git source `{}`", self.git.repository);
86+
87+
// Report the checkout operation to the reporter.
88+
let task = self.reporter.as_ref().map(|reporter| {
89+
reporter.on_checkout_start(remote.url(), self.git.reference.as_rev())
90+
});
91+
92+
let (db, actual_rev) = remote.checkout(
93+
&db_path,
94+
db,
95+
&self.git.reference,
96+
locked_rev.map(GitOid::from),
97+
&self.client,
98+
)?;
99+
100+
(db, GitSha::from(actual_rev), task)
101+
}
102+
};
103+
104+
let short_id = db.to_short_id(actual_rev.into())?;
105+
106+
// Report the checkout operation to the reporter.
107+
if let Some(task) = task {
108+
if let Some(reporter) = self.reporter.as_ref() {
109+
reporter.on_checkout_complete(remote.url(), short_id.as_str(), task);
110+
}
111+
}
112+
113+
Ok(actual_rev)
114+
}
115+
54116
/// Fetch the underlying Git repository at the given revision.
55117
#[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))]
56118
pub fn fetch(self) -> Result<Fetch> {

crates/uv-resolver/src/redirect.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use url::Url;
2-
32
use uv_git::{GitReference, GitResolver};
43
use uv_pep508::VerbatimUrl;
54
use uv_pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl};
@@ -17,9 +16,8 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba
1716
let Some(new_git_url) = git.precise(git_url.clone()) else {
1817
debug_assert!(
1918
matches!(git_url.reference(), GitReference::FullCommit(_)),
20-
"Unseen Git URL: {}, {:?}",
19+
"Unseen Git URL: {}, {git_url:?}",
2120
url.verbatim,
22-
git_url
2321
);
2422
return url;
2523
};

0 commit comments

Comments
 (0)