From 72a844ce771dcaf075e04da908abf6501983b530 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Mon, 15 Sep 2025 19:30:55 +0530 Subject: [PATCH 1/6] preserve relative paths in lockfiles --- crates/uv-resolver/src/lock/mod.rs | 20 +++++- crates/uv/tests/it/lock.rs | 110 +++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index fed3a623a..4e445c94c 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -3566,9 +3566,23 @@ impl Source { .to_file_path() .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?; let path = relative_to(&path, root) - .or_else(|_| std::path::absolute(&path)) - .map_err(LockErrorKind::IndexRelativePath)?; - let source = RegistrySource::Path(path.into_boxed_path()); + .or_else(|_| { + // If relative_to fails, check if the user originally provided a relative path + // that we should preserve (for flat indices) + if let Some(given) = url.given() { + let given_path = Path::new(given); + if given_path.is_relative() { + // Keep the original relative path for flat indices + return Ok(given_path.to_path_buf()); + } + } + // Default fallback behavior + std::path::absolute(&path) + }) + .map_err(LockErrorKind::IndexRelativePath)? + .into_boxed_path(); + + let source = RegistrySource::Path(path); Ok(Self::Registry(source)) } } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 7f7372fcc..ffa0d127f 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -11180,6 +11180,116 @@ fn lock_find_links_relative_url() -> Result<()> { Ok(()) } +/// Ensure that flat indices preserve relative paths in lockfiles (issue with flat index portability). +#[test] +fn lock_find_links_relative_path_preserved() -> Result<()> { + let context = TestContext::new("3.12"); + + // Populate the `--find-links` entries in a subdirectory. + fs_err::create_dir_all(context.temp_dir.join("local_packages"))?; + + for entry in fs_err::read_dir(context.workspace_root.join("scripts/links"))? { + let entry = entry?; + let path = entry.path(); + if path + .file_name() + .and_then(|file_name| file_name.to_str()) + .is_some_and(|file_name| file_name.starts_with("tqdm-")) + { + let dest = context + .temp_dir + .join("local_packages") + .join(path.file_name().unwrap()); + fs_err::copy(&path, &dest)?; + } + } + + let workspace = context.temp_dir.child("workspace"); + + let pyproject_toml = workspace.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["tqdm"] + + [[tool.uv.index]] + name = "local" + format = "flat" + url = "../local_packages" + explicit = true + + [tool.uv.sources] + tqdm = { index = "local" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().current_dir(&workspace), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 2 packages in [TIME] + "); + + let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "tqdm" }, + ] + + [package.metadata] + requires-dist = [{ name = "tqdm", index = "file://[TEMP_DIR]/local_packages" }] + + [[package]] + name = "tqdm" + version = "1000.0.0" + source = { registry = "../local_packages" } + wheels = [ + { path = "tqdm-1000.0.0-py3-none-any.whl" }, + ] + "# + ); + }); + + // Verify that the lockfile contains relative paths as specified by the user + assert!(lock.contains(r#"source = { registry = "../local_packages" }"#), + "Lockfile should preserve relative path for flat index as specified by user"); + + // Re-run with `--locked` to ensure the lockfile is valid. + uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 2 packages in [TIME] + "); + + Ok(()) +} + /// Lock a local source distribution via `--find-links`. #[test] fn lock_find_links_local_sdist() -> Result<()> { From 19e005bf0fc9e7a58f2f0b47fbffb658bc2ac627 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Mon, 15 Sep 2025 23:14:31 +0530 Subject: [PATCH 2/6] fix formatting and use normalize_path --- crates/uv-resolver/src/lock/mod.rs | 4 ++-- crates/uv/tests/it/lock.rs | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 4e445c94c..84208896b 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -3573,7 +3573,7 @@ impl Source { let given_path = Path::new(given); if given_path.is_relative() { // Keep the original relative path for flat indices - return Ok(given_path.to_path_buf()); + return Ok(uv_fs::normalize_path(given_path).into_owned()); } } // Default fallback behavior @@ -3581,7 +3581,7 @@ impl Source { }) .map_err(LockErrorKind::IndexRelativePath)? .into_boxed_path(); - + let source = RegistrySource::Path(path); Ok(Self::Registry(source)) } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index ffa0d127f..7e184cf44 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -11272,10 +11272,6 @@ fn lock_find_links_relative_path_preserved() -> Result<()> { ); }); - // Verify that the lockfile contains relative paths as specified by the user - assert!(lock.contains(r#"source = { registry = "../local_packages" }"#), - "Lockfile should preserve relative path for flat index as specified by user"); - // Re-run with `--locked` to ensure the lockfile is valid. uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r" success: true From 46257944f4681e7dd900715061632d592fbe91e0 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 19 Sep 2025 20:02:40 +0530 Subject: [PATCH 3/6] update --- .../uv-distribution-types/src/requirement.rs | 32 +++++++++++++++---- crates/uv/tests/it/lock.rs | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/crates/uv-distribution-types/src/requirement.rs b/crates/uv-distribution-types/src/requirement.rs index 25676d999..8826793ef 100644 --- a/crates/uv-distribution-types/src/requirement.rs +++ b/crates/uv-distribution-types/src/requirement.rs @@ -799,7 +799,7 @@ enum RequirementSourceWire { Registry { #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] specifier: VersionSpecifiers, - index: Option, + index: Option, conflict: Option, }, } @@ -812,9 +812,22 @@ impl From for RequirementSourceWire { index, conflict, } => { - let index = index.map(|index| index.url.into_url()).map(|mut index| { - index.remove_credentials(); - index + let index = index.map(|index| { + // Check if this is a path index that was originally relative + if let IndexUrl::Path(path_url) = &index.url { + if let Some(given) = path_url.given() { + let given_path = Path::new(given); + // For relative paths, preserve the original string directly + if given_path.is_relative() && !given.starts_with("file://") { + return given.to_string(); + } + } + } + + // Default behavior for absolute paths and non-path URLs + let mut url = index.url.into_url(); + url.remove_credentials(); + url.to_string() }); Self::Registry { specifier, @@ -923,8 +936,15 @@ impl TryFrom for RequirementSource { conflict, } => Ok(Self::Registry { specifier, - index: index - .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))), + index: index.map(|index_str| { + // Try to parse as URL first, then fallback to path + if let Ok(verbatim_url) = VerbatimUrl::from_url_or_path(&index_str, None) { + IndexMetadata::from(IndexUrl::from(verbatim_url.with_given(&index_str))) + } else { + // provide a fallback + IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(DisplaySafeUrl::parse(&index_str).unwrap()))) + } + }), conflict, }), RequirementSourceWire::Git { git } => { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 7e184cf44..efc4df3fd 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -11259,7 +11259,7 @@ fn lock_find_links_relative_path_preserved() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "tqdm", index = "file://[TEMP_DIR]/local_packages" }] + requires-dist = [{ name = "tqdm", index = "../local_packages" }] [[package]] name = "tqdm" From 62d2b5eee08ec47f401ac6b38a361030fdb74ef9 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 19 Sep 2025 20:15:07 +0530 Subject: [PATCH 4/6] fix formatting --- crates/uv-distribution-types/src/requirement.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/uv-distribution-types/src/requirement.rs b/crates/uv-distribution-types/src/requirement.rs index 8826793ef..864d018a7 100644 --- a/crates/uv-distribution-types/src/requirement.rs +++ b/crates/uv-distribution-types/src/requirement.rs @@ -823,7 +823,7 @@ impl From for RequirementSourceWire { } } } - + // Default behavior for absolute paths and non-path URLs let mut url = index.url.into_url(); url.remove_credentials(); @@ -942,7 +942,9 @@ impl TryFrom for RequirementSource { IndexMetadata::from(IndexUrl::from(verbatim_url.with_given(&index_str))) } else { // provide a fallback - IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(DisplaySafeUrl::parse(&index_str).unwrap()))) + IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url( + DisplaySafeUrl::parse(&index_str).unwrap(), + ))) } }), conflict, From ce1bb7311d1a0747f554ff5ea14baba199c9e714 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 31 Oct 2025 16:04:51 +0530 Subject: [PATCH 5/6] refactor: remove redundant fallback with improved error handling --- .../uv-distribution-types/src/requirement.rs | 31 ++++++++++--------- crates/uv/tests/it/lock.rs | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/uv-distribution-types/src/requirement.rs b/crates/uv-distribution-types/src/requirement.rs index 864d018a7..e671c3a86 100644 --- a/crates/uv-distribution-types/src/requirement.rs +++ b/crates/uv-distribution-types/src/requirement.rs @@ -934,21 +934,22 @@ impl TryFrom for RequirementSource { specifier, index, conflict, - } => Ok(Self::Registry { - specifier, - index: index.map(|index_str| { - // Try to parse as URL first, then fallback to path - if let Ok(verbatim_url) = VerbatimUrl::from_url_or_path(&index_str, None) { - IndexMetadata::from(IndexUrl::from(verbatim_url.with_given(&index_str))) - } else { - // provide a fallback - IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url( - DisplaySafeUrl::parse(&index_str).unwrap(), - ))) - } - }), - conflict, - }), + } => { + let index = if let Some(index_str) = index { + let verbatim_url = VerbatimUrl::from_url_or_path(&index_str, None)?; + Some(IndexMetadata::from(IndexUrl::from( + verbatim_url.with_given(&index_str), + ))) + } else { + None + }; + + Ok(Self::Registry { + specifier, + index, + conflict, + }) + } RequirementSourceWire::Git { git } => { let mut repository = DisplaySafeUrl::parse(&git)?; diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index f46bff955..a29fc907b 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -11236,7 +11236,7 @@ fn lock_find_links_relative_path_preserved() -> Result<()> { Resolved 2 packages in [TIME] "); - let lock = fs_err::read_to_string(workspace.join("uv.lock")).unwrap(); + let lock = fs_err::read_to_string(workspace.join("uv.lock"))?; insta::with_settings!({ filters => context.filters(), From 81c6eefafca60769108432f3205fb0e677a138d1 Mon Sep 17 00:00:00 2001 From: Harshith VH Date: Fri, 7 Nov 2025 01:14:25 +0530 Subject: [PATCH 6/6] add docs: why both relative_to and url.given() are needed --- crates/uv-resolver/src/lock/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index b419a1796..0c0b1aa8d 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -3581,18 +3581,18 @@ impl Source { let path = url .to_file_path() .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?; + // Try to compute a relative path from the project root. If that fails (e.g., paths + // on different drives), preserve the original relative path from the user's config + // via `url.given()` if available, since flat indices should remain relative. + // Otherwise, fall back to an absolute path. let path = relative_to(&path, root) .or_else(|_| { - // If relative_to fails, check if the user originally provided a relative path - // that we should preserve (for flat indices) if let Some(given) = url.given() { let given_path = Path::new(given); if given_path.is_relative() { - // Keep the original relative path for flat indices return Ok(uv_fs::normalize_path(given_path).into_owned()); } } - // Default fallback behavior std::path::absolute(&path) }) .map_err(LockErrorKind::IndexRelativePath)?