Skip to content

Commit

Permalink
Use consistent canonicalization for URLs (#5980)
Browse files Browse the repository at this point in the history
Right now, the URL gets out-of-sync with the install path, since the
install path is canonicalized. This leads to a subtle error on Windows
(in CI) in which we don't preserve caching across resolution and
installation.
  • Loading branch information
charliermarsh authored Aug 10, 2024
1 parent cd0171a commit 19ac9af
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 38 deletions.
16 changes: 8 additions & 8 deletions crates/distribution-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub struct DirectUrlBuiltDist {
#[derive(Debug, Clone, Hash)]
pub struct PathBuiltDist {
pub filename: WheelFilename,
/// The resolved, absolute path to the wheel which we use for installing.
/// The absolute, canonicalized path to the wheel which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the wheel
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand Down Expand Up @@ -277,7 +277,7 @@ pub struct GitSourceDist {
#[derive(Debug, Clone, Hash)]
pub struct PathSourceDist {
pub name: PackageName,
/// The resolved, absolute path to the distribution which we use for installing.
/// The absolute, canonicalized path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand All @@ -293,7 +293,7 @@ pub struct PathSourceDist {
#[derive(Debug, Clone, Hash)]
pub struct DirectorySourceDist {
pub name: PackageName,
/// The resolved, absolute path to the distribution which we use for installing.
/// The absolute, canonicalized path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand Down Expand Up @@ -354,7 +354,7 @@ impl Dist {
ext: DistExtension,
) -> Result<Dist, Error> {
// Store the canonicalized path, which also serves to validate that it exists.
let canonicalized_path = match install_path.canonicalize() {
let install_path = match install_path.canonicalize() {
Ok(path) => path,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::NotFound(url.to_url()));
Expand All @@ -376,14 +376,14 @@ impl Dist {
}
Ok(Self::Built(BuiltDist::Path(PathBuiltDist {
filename,
install_path: canonicalized_path.clone(),
install_path,
lock_path: lock_path.to_path_buf(),
url,
})))
}
DistExtension::Source(ext) => Ok(Self::Source(SourceDist::Path(PathSourceDist {
name,
install_path: canonicalized_path.clone(),
install_path,
lock_path: lock_path.to_path_buf(),
ext,
url,
Expand All @@ -400,7 +400,7 @@ impl Dist {
editable: bool,
) -> Result<Dist, Error> {
// Store the canonicalized path, which also serves to validate that it exists.
let canonicalized_path = match install_path.canonicalize() {
let install_path = match install_path.canonicalize() {
Ok(path) => path,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::NotFound(url.to_url()));
Expand All @@ -411,7 +411,7 @@ impl Dist {
// Determine whether the path represents an archive or a directory.
Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
name,
install_path: canonicalized_path.clone(),
install_path,
lock_path: lock_path.to_path_buf(),
editable,
url,
Expand Down
28 changes: 18 additions & 10 deletions crates/pep508-rs/src/verbatim_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::sync::LazyLock;
use thiserror::Error;
use url::{ParseError, Url};

use uv_fs::{normalize_absolute_path, normalize_url_path};
use uv_fs::{normalize_absolute_path, normalize_url_path, Simplified};

use crate::Pep508Url;

Expand Down Expand Up @@ -36,9 +36,12 @@ impl VerbatimUrl {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, VerbatimUrlError> {
let path = path.as_ref();

// Normalize the path.
let path = normalize_absolute_path(path)
.map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?;
// Normalize the path (and canonicalize it, if possible).
let path = match path.simple_canonicalize() {
Ok(path) => path,
Err(_) => normalize_absolute_path(path)
.map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?,
};

// Extract the fragment, if it exists.
let (path, fragment) = split_fragment(&path);
Expand Down Expand Up @@ -77,9 +80,12 @@ impl VerbatimUrl {
base_dir.as_ref().join(path)
};

// Normalize the path.
let path = normalize_absolute_path(&path)
.map_err(|err| VerbatimUrlError::Normalization(path.clone(), err))?;
// Normalize the path (and canonicalize it, if possible).
let path = match path.simple_canonicalize() {
Ok(path) => path,
Err(_) => normalize_absolute_path(&path)
.map_err(|err| VerbatimUrlError::Normalization(path.clone(), err))?,
};

// Extract the fragment, if it exists.
let (path, fragment) = split_fragment(&path);
Expand Down Expand Up @@ -107,9 +113,11 @@ impl VerbatimUrl {
return Err(VerbatimUrlError::WorkingDirectory(path.to_path_buf()));
};

// Normalize the path.
let Ok(path) = normalize_absolute_path(&path) else {
return Err(VerbatimUrlError::WorkingDirectory(path));
// Normalize the path (and canonicalize it, if possible).
let path = match path.simple_canonicalize() {
Ok(path) => path,
Err(_) => normalize_absolute_path(&path)
.map_err(|_| VerbatimUrlError::WorkingDirectory(path))?,
};

// Extract the fragment, if it exists.
Expand Down
4 changes: 2 additions & 2 deletions crates/pypi-types/src/parsed_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ impl ParsedUrl {
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)]
pub struct ParsedPathUrl {
pub url: Url,
/// The resolved, absolute path to the distribution which we use for installing.
/// The absolute, canonicalized path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand Down Expand Up @@ -219,7 +219,7 @@ impl ParsedPathUrl {
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)]
pub struct ParsedDirectoryUrl {
pub url: Url,
/// The resolved, absolute path to the distribution which we use for installing.
/// The absolute, canonicalized path to the distribution which we use for installing.
pub install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand Down
4 changes: 2 additions & 2 deletions crates/pypi-types/src/requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ pub enum RequirementSource {
/// be a binary distribution (a `.whl` file) or a source distribution archive (a `.zip` or
/// `.tar.gz` file).
Path {
/// The resolved, absolute path to the distribution which we use for installing.
/// The absolute, canonicalized path to the distribution which we use for installing.
install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand All @@ -328,7 +328,7 @@ pub enum RequirementSource {
/// A local source tree (a directory with a pyproject.toml in, or a legacy
/// source distribution with only a setup.py but non pyproject.toml in it).
Directory {
/// The resolved, absolute path to the distribution which we use for installing.
/// The absolute, canonicalized path to the distribution which we use for installing.
install_path: PathBuf,
/// The absolute path or path relative to the workspace root pointing to the distribution
/// which we use for locking. Unlike `given` on the verbatim URL all environment variables
Expand Down
10 changes: 5 additions & 5 deletions crates/uv-installer/src/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ impl<'a> Planner<'a> {
lock_path,
} => {
// Store the canonicalized path, which also serves to validate that it exists.
let path = match install_path.canonicalize() {
let install_path = match install_path.canonicalize() {
Ok(path) => path,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::NotFound(url.to_url()).into());
Expand All @@ -280,7 +280,7 @@ impl<'a> Planner<'a> {
let sdist = DirectorySourceDist {
name: requirement.name.clone(),
url: url.clone(),
install_path: path,
install_path,
lock_path: lock_path.clone(),
editable: *editable,
};
Expand All @@ -306,7 +306,7 @@ impl<'a> Planner<'a> {
lock_path,
} => {
// Store the canonicalized path, which also serves to validate that it exists.
let path = match install_path.canonicalize() {
let install_path = match install_path.canonicalize() {
Ok(path) => path,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::NotFound(url.to_url()).into());
Expand All @@ -330,7 +330,7 @@ impl<'a> Planner<'a> {
let wheel = PathBuiltDist {
filename,
url: url.clone(),
install_path: install_path.clone(),
install_path,
lock_path: lock_path.clone(),
};

Expand Down Expand Up @@ -382,7 +382,7 @@ impl<'a> Planner<'a> {
let sdist = PathSourceDist {
name: requirement.name.clone(),
url: url.clone(),
install_path: path,
install_path,
lock_path: lock_path.clone(),
ext: *ext,
};
Expand Down
17 changes: 6 additions & 11 deletions crates/uv/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,12 +682,7 @@ fn sync_build_isolation_package() -> Result<()> {
/// Test that relative wheel paths are correctly preserved.
#[test]
fn sync_relative_wheel() -> Result<()> {
// TODO(charlie): On Windows, this test currently prepares two packages (vs. one of Unix). This
// is due to an error in path canonicalization, and GitHub Actions on Windows have a symlink
// in the path.
//
// See: https://github.com/astral-sh/uv/issues/5979
let context = TestContext::new("3.12").with_filtered_counts();
let context = TestContext::new("3.12");

let requirements = r#"[project]
name = "relative_wheel"
Expand Down Expand Up @@ -727,9 +722,9 @@ fn sync_relative_wheel() -> Result<()> {
----- stderr -----
warning: `uv sync` is experimental and may change without warning
warning: `uv.sources` is experimental and may change without warning
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 2 packages in [TIME]
+ ok==1.0.0 (from file://[TEMP_DIR]/wheels/ok-1.0.0-py3-none-any.whl)
+ relative-wheel==0.1.0 (from file://[TEMP_DIR]/)
"###);
Expand Down Expand Up @@ -778,8 +773,8 @@ fn sync_relative_wheel() -> Result<()> {
----- stderr -----
warning: `uv sync` is experimental and may change without warning
warning: `uv.sources` is experimental and may change without warning
Resolved [N] packages in [TIME]
Audited [N] packages in [TIME]
Resolved 2 packages in [TIME]
Audited 2 packages in [TIME]
"###);

Ok(())
Expand Down

0 comments on commit 19ac9af

Please sign in to comment.