Support file://localhost/ schemes (#2657)

## Summary

`uv` was failing to install requirements defined like:

```
file://localhost/Users/crmarsh/Downloads/iniconfig-2.0.0-py3-none-any.whl
```

Closes https://github.com/astral-sh/uv/issues/2652.
This commit is contained in:
Charlie Marsh 2024-03-25 15:23:26 -04:00 committed by GitHub
parent 7a5571fa5c
commit 8587c440fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 119 additions and 13 deletions

View file

@ -9,7 +9,7 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use url::Url;
use pep508_rs::{expand_env_vars, split_scheme, Scheme, VerbatimUrl};
use pep508_rs::{expand_env_vars, split_scheme, strip_host, Scheme, VerbatimUrl};
use uv_fs::normalize_url_path;
use crate::Verbatim;
@ -124,9 +124,10 @@ impl FromStr for FlatIndexLocation {
// Parse the expanded path.
if let Some((scheme, path)) = split_scheme(&expanded) {
match Scheme::parse(scheme) {
// Ex) `file:///home/ferris/project/scripts/...` or `file:../ferris/`
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
Some(Scheme::File) => {
let path = path.strip_prefix("//").unwrap_or(path);
// Strip the leading slashes, along with the `localhost` host, if present.
let path = strip_host(path);
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
let path = normalize_url_path(path);

View file

@ -46,7 +46,7 @@ use uv_fs::normalize_url_path;
// Parity with the crates.io version of pep508_rs
use crate::verbatim_url::VerbatimUrlError;
pub use uv_normalize::{ExtraName, InvalidNameError, PackageName};
pub use verbatim_url::{expand_env_vars, split_scheme, Scheme, VerbatimUrl};
pub use verbatim_url::{expand_env_vars, split_scheme, strip_host, Scheme, VerbatimUrl};
mod marker;
mod verbatim_url;
@ -1018,9 +1018,10 @@ fn preprocess_url(
if let Some((scheme, path)) = split_scheme(&expanded) {
match Scheme::parse(scheme) {
// Ex) `file:///home/ferris/project/scripts/...` or `file:../editable/`.
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
Some(Scheme::File) => {
let path = path.strip_prefix("//").unwrap_or(path);
// Strip the leading slashes, along with the `localhost` host, if present.
let path = strip_host(path);
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
let path = normalize_url_path(path);
@ -1156,9 +1157,10 @@ fn preprocess_unnamed_url(
if let Some((scheme, path)) = split_scheme(&expanded) {
match Scheme::parse(scheme) {
// Ex) `file:///home/ferris/project/scripts/...` or `file:../editable/`.
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
Some(Scheme::File) => {
let path = path.strip_prefix("//").unwrap_or(path);
// Strip the leading slashes, along with the `localhost` host, if present.
let path = strip_host(path);
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
let path = normalize_url_path(path);

View file

@ -264,6 +264,24 @@ pub fn split_scheme(s: &str) -> Option<(&str, &str)> {
Some((scheme, rest))
}
/// Strip the `file://localhost/` host from a file path.
pub fn strip_host(path: &str) -> &str {
// Ex) `file://localhost/...`.
if let Some(path) = path
.strip_prefix("//localhost")
.filter(|path| path.starts_with('/'))
{
return path;
}
// Ex) `file:///...`.
if let Some(path) = path.strip_prefix("//") {
return path;
}
path
}
/// Split the fragment from a URL.
///
/// For example, given `file:///home/ferris/project/scripts#hash=somehash`, returns

View file

@ -46,7 +46,7 @@ use unscanny::{Pattern, Scanner};
use url::Url;
use pep508_rs::{
expand_env_vars, split_scheme, Extras, Pep508Error, Pep508ErrorSource, Requirement,
expand_env_vars, split_scheme, strip_host, Extras, Pep508Error, Pep508ErrorSource, Requirement,
RequirementsTxtRequirement, Scheme, VerbatimUrl,
};
#[cfg(feature = "http")]
@ -104,9 +104,10 @@ impl FindLink {
if let Some((scheme, path)) = split_scheme(&expanded) {
match Scheme::parse(scheme) {
// Ex) `file:///home/ferris/project/scripts/...` or `file:../ferris/`
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
Some(Scheme::File) => {
let path = path.strip_prefix("//").unwrap_or(path);
// Strip the leading slashes, along with the `localhost` host, if present.
let path = strip_host(path);
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
let path = normalize_url_path(path);
@ -221,7 +222,8 @@ impl EditableRequirement {
match Scheme::parse(scheme) {
// Ex) `file:///home/ferris/project/scripts/...` or `file:../editable/`
Some(Scheme::File) => {
let path = path.strip_prefix("//").unwrap_or(path);
// Strip the leading slashes, along with the `localhost` host, if present.
let path = strip_host(path);
// Transform, e.g., `/C:/Users/ferris/wheel-0.42.0.tar.gz` to `C:\Users\ferris\wheel-0.42.0.tar.gz`.
let path = normalize_url_path(path);

View file

@ -1284,7 +1284,7 @@ pub async fn download_and_extract_archive(
client: &RegistryClient,
) -> Result<ExtractedSource, Error> {
match Scheme::parse(url.scheme()) {
// Ex) `file:///home/ferris/project/scripts/...` or `file:../editable/`.
// Ex) `file:///home/ferris/project/scripts/...`, `file://localhost/home/ferris/project/scripts/...`, or `file:../ferris/`
Some(Scheme::File) => {
let path = url.to_file_path().expect("URL to be a file path");
extract_archive(&path, cache).await

View file

@ -2053,6 +2053,89 @@ fn compile_wheel_path_dependency() -> Result<()> {
"###
);
// Run the same operation, but this time with an absolute path (rather than a URL), including
// the `file://` prefix.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!("flask @ file://{}", flask_wheel.path().display()))?;
// In addition to the standard filters, remove the temporary directory from the snapshot.
let filter_path = regex::escape(&flask_wheel.user_display().to_string());
let filters: Vec<_> = [(filter_path.as_str(), "/[TEMP_DIR]/")]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect();
uv_snapshot!(filters, context.compile()
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
blinker==1.7.0
# via flask
click==8.1.7
# via flask
flask @ file:///[TEMP_DIR]/
itsdangerous==2.1.2
# via flask
jinja2==3.1.3
# via flask
markupsafe==2.1.5
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###
);
// Run the same operation, but this time with an absolute path (rather than a URL), including
// the `file://localhost/` prefix.
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(&format!(
"flask @ file://localhost/{}",
flask_wheel.path().display()
))?;
// In addition to the standard filters, remove the temporary directory from the snapshot.
let filter_path = regex::escape(&flask_wheel.user_display().to_string());
let filters: Vec<_> = [(filter_path.as_str(), "/[TEMP_DIR]/")]
.into_iter()
.chain(INSTA_FILTERS.to_vec())
.collect();
uv_snapshot!(filters, context.compile()
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
blinker==1.7.0
# via flask
click==8.1.7
# via flask
flask @ file://localhost//[TEMP_DIR]/
itsdangerous==2.1.2
# via flask
jinja2==3.1.3
# via flask
markupsafe==2.1.5
# via
# jinja2
# werkzeug
werkzeug==3.0.1
# via flask
----- stderr -----
Resolved 7 packages in [TIME]
"###
);
Ok(())
}