Allow local indexes to reference remote files (#14294)

## Summary

Previously, we assumed that local indexes only referenced local files.
However, it's fine for a local index (like, a `file://`-based Simple
API) to reference a remote file, and in fact Pyodide operates this way.

Closes https://github.com/astral-sh/uv/issues/14227.

## Test Plan

Ran `UV_INDEX=$(pyodide config get package_index) cargo run add anyio`,
which produced this lockfile:

```toml
version = 1
revision = 2
requires-python = ">=3.13.2"

[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "../../../Library/Caches/.pyodide-xbuildenv-0.30.5/0.27.7/xbuildenv/pyodide-root/package_index" }
dependencies = [
    { name = "idna" },
    { name = "sniffio" },
]
wheels = [
    { url = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/anyio-4.9.0-py3-none-any.whl", hash = "sha256:e1d9180d4361fd71d1bc4a7007fea6cae1d18792dba9d07eaad89f2a8562f71c" },
]

[[package]]
name = "foo"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
    { name = "anyio" },
]

[package.metadata]
requires-dist = [{ name = "anyio", specifier = ">=4.9.0" }]

[[package]]
name = "idna"
version = "3.7"
source = { registry = "../../../Library/Caches/.pyodide-xbuildenv-0.30.5/0.27.7/xbuildenv/pyodide-root/package_index" }
wheels = [
    { url = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/idna-3.7-py3-none-any.whl", hash = "sha256:9d4685891e3e37434e09b1becda7e96a284e660c7aea9222564d88b6c3527c09" },
]

[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "../../../Library/Caches/.pyodide-xbuildenv-0.30.5/0.27.7/xbuildenv/pyodide-root/package_index" }
wheels = [
    { url = "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:9215f9917b34fc73152b134a3fc0a2eb0e4a49b0b956100cad75e84943412bb9" },
]
```
This commit is contained in:
Charlie Marsh 2025-06-26 16:17:42 -04:00 committed by GitHub
parent 05ab266200
commit 326e4497da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2541,16 +2541,30 @@ impl Package {
.as_ref() .as_ref()
.expect("version for registry source"); .expect("version for registry source");
let file_path = sdist.path().ok_or_else(|| LockErrorKind::MissingPath { let file_url = match sdist {
name: name.clone(), SourceDist::Url { url: file_url, .. } => {
version: version.clone(), FileLocation::AbsoluteUrl(file_url.clone())
})?;
let file_path = workspace_root.join(path).join(file_path);
let file_url = DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
LockErrorKind::PathToUrl {
path: file_path.into_boxed_path(),
} }
})?; SourceDist::Path {
path: file_path, ..
} => {
let file_path = workspace_root.join(path).join(file_path);
let file_url =
DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
LockErrorKind::PathToUrl {
path: file_path.into_boxed_path(),
}
})?;
FileLocation::AbsoluteUrl(UrlString::from(file_url))
}
SourceDist::Metadata { .. } => {
return Err(LockErrorKind::MissingPath {
name: name.clone(),
version: version.clone(),
}
.into());
}
};
let filename = sdist let filename = sdist
.filename() .filename()
.ok_or_else(|| LockErrorKind::MissingFilename { .ok_or_else(|| LockErrorKind::MissingFilename {
@ -2571,9 +2585,10 @@ impl Package {
requires_python: None, requires_python: None,
size: sdist.size(), size: sdist.size(),
upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond), upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
url: FileLocation::AbsoluteUrl(UrlString::from(file_url)), url: file_url,
yanked: None, yanked: None,
}); });
let index = IndexUrl::from( let index = IndexUrl::from(
VerbatimUrl::from_absolute_path(workspace_root.join(path)) VerbatimUrl::from_absolute_path(workspace_root.join(path))
.map_err(LockErrorKind::RegistryVerbatimUrl)?, .map_err(LockErrorKind::RegistryVerbatimUrl)?,
@ -3688,14 +3703,6 @@ impl SourceDist {
} }
} }
fn path(&self) -> Option<&Path> {
match &self {
SourceDist::Metadata { .. } => None,
SourceDist::Url { .. } => None,
SourceDist::Path { path, .. } => Some(path),
}
}
pub(crate) fn hash(&self) -> Option<&Hash> { pub(crate) fn hash(&self) -> Option<&Hash> {
match &self { match &self {
SourceDist::Metadata { metadata } => metadata.hash.as_ref(), SourceDist::Metadata { metadata } => metadata.hash.as_ref(),
@ -3818,34 +3825,57 @@ impl SourceDist {
let index_path = path let index_path = path
.to_file_path() .to_file_path()
.map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?; .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
let reg_dist_url = reg_dist let url = reg_dist
.file .file
.url .url
.to_url() .to_url()
.map_err(LockErrorKind::InvalidUrl)?; .map_err(LockErrorKind::InvalidUrl)?;
let reg_dist_path = reg_dist_url
.to_file_path() if url.scheme() == "file" {
.map_err(|()| LockErrorKind::UrlToPath { url: reg_dist_url })?; let reg_dist_path = url
let path = relative_to(&reg_dist_path, index_path) .to_file_path()
.or_else(|_| std::path::absolute(&reg_dist_path)) .map_err(|()| LockErrorKind::UrlToPath { url })?;
.map_err(LockErrorKind::DistributionRelativePath)? let path = relative_to(&reg_dist_path, index_path)
.into_boxed_path(); .or_else(|_| std::path::absolute(&reg_dist_path))
let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from); .map_err(LockErrorKind::DistributionRelativePath)?
let size = reg_dist.file.size; .into_boxed_path();
let upload_time = reg_dist let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
.file let size = reg_dist.file.size;
.upload_time_utc_ms let upload_time = reg_dist
.map(Timestamp::from_millisecond) .file
.transpose() .upload_time_utc_ms
.map_err(LockErrorKind::InvalidTimestamp)?; .map(Timestamp::from_millisecond)
Ok(Some(SourceDist::Path { .transpose()
path, .map_err(LockErrorKind::InvalidTimestamp)?;
metadata: SourceDistMetadata { Ok(Some(SourceDist::Path {
hash, path,
size, metadata: SourceDistMetadata {
upload_time, hash,
}, size,
})) upload_time,
},
}))
} else {
let url = normalize_file_location(&reg_dist.file.url)
.map_err(LockErrorKind::InvalidUrl)
.map_err(LockError::from)?;
let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
let size = reg_dist.file.size;
let upload_time = reg_dist
.file
.upload_time_utc_ms
.map(Timestamp::from_millisecond)
.transpose()
.map_err(LockErrorKind::InvalidTimestamp)?;
Ok(Some(SourceDist::Url {
url,
metadata: SourceDistMetadata {
hash,
size,
upload_time,
},
}))
}
} }
} }
} }
@ -4152,20 +4182,42 @@ impl Wheel {
.to_file_path() .to_file_path()
.map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?; .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?; let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
let wheel_path = wheel_url
.to_file_path() if wheel_url.scheme() == "file" {
.map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?; let wheel_path = wheel_url
let path = relative_to(&wheel_path, index_path) .to_file_path()
.or_else(|_| std::path::absolute(&wheel_path)) .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
.map_err(LockErrorKind::DistributionRelativePath)? let path = relative_to(&wheel_path, index_path)
.into_boxed_path(); .or_else(|_| std::path::absolute(&wheel_path))
Ok(Wheel { .map_err(LockErrorKind::DistributionRelativePath)?
url: WheelWireSource::Path { path }, .into_boxed_path();
hash: None, Ok(Wheel {
size: None, url: WheelWireSource::Path { path },
upload_time: None, hash: None,
filename, size: None,
}) upload_time: None,
filename,
})
} else {
let url = normalize_file_location(&wheel.file.url)
.map_err(LockErrorKind::InvalidUrl)
.map_err(LockError::from)?;
let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
let size = wheel.file.size;
let upload_time = wheel
.file
.upload_time_utc_ms
.map(Timestamp::from_millisecond)
.transpose()
.map_err(LockErrorKind::InvalidTimestamp)?;
Ok(Wheel {
url: WheelWireSource::Url { url },
hash,
size,
filename,
upload_time,
})
}
} }
} }
} }
@ -4203,8 +4255,10 @@ impl Wheel {
match source { match source {
RegistrySource::Url(url) => { RegistrySource::Url(url) => {
let file_url = match &self.url { let file_location = match &self.url {
WheelWireSource::Url { url } => url, WheelWireSource::Url { url: file_url } => {
FileLocation::AbsoluteUrl(file_url.clone())
}
WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => { WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
return Err(LockErrorKind::MissingUrl { return Err(LockErrorKind::MissingUrl {
name: filename.name, name: filename.name,
@ -4220,7 +4274,7 @@ impl Wheel {
requires_python: None, requires_python: None,
size: self.size, size: self.size,
upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond), upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
url: FileLocation::AbsoluteUrl(file_url.clone()), url: file_location,
yanked: None, yanked: None,
}); });
let index = IndexUrl::from(VerbatimUrl::from_url( let index = IndexUrl::from(VerbatimUrl::from_url(
@ -4233,9 +4287,21 @@ impl Wheel {
}) })
} }
RegistrySource::Path(index_path) => { RegistrySource::Path(index_path) => {
let file_path = match &self.url { let file_location = match &self.url {
WheelWireSource::Path { path } => path, WheelWireSource::Url { url: file_url } => {
WheelWireSource::Url { .. } | WheelWireSource::Filename { .. } => { FileLocation::AbsoluteUrl(file_url.clone())
}
WheelWireSource::Path { path: file_path } => {
let file_path = root.join(index_path).join(file_path);
let file_url =
DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
LockErrorKind::PathToUrl {
path: file_path.into_boxed_path(),
}
})?;
FileLocation::AbsoluteUrl(UrlString::from(file_url))
}
WheelWireSource::Filename { .. } => {
return Err(LockErrorKind::MissingPath { return Err(LockErrorKind::MissingPath {
name: filename.name, name: filename.name,
version: filename.version, version: filename.version,
@ -4243,12 +4309,6 @@ impl Wheel {
.into()); .into());
} }
}; };
let file_path = root.join(index_path).join(file_path);
let file_url = DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
LockErrorKind::PathToUrl {
path: file_path.into_boxed_path(),
}
})?;
let file = Box::new(uv_distribution_types::File { let file = Box::new(uv_distribution_types::File {
dist_info_metadata: false, dist_info_metadata: false,
filename: SmallString::from(filename.to_string()), filename: SmallString::from(filename.to_string()),
@ -4256,7 +4316,7 @@ impl Wheel {
requires_python: None, requires_python: None,
size: self.size, size: self.size,
upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond), upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
url: FileLocation::AbsoluteUrl(UrlString::from(file_url)), url: file_location,
yanked: None, yanked: None,
}); });
let index = IndexUrl::from( let index = IndexUrl::from(