fix: preserve absolute paths in lockfile when user specifies absolute find-links

When a user specifies an absolute path for find-links or path dependencies
(e.g., `/shared_drive/wheels`), preserve the absolute path in uv.lock
instead of converting it to a relative path like `../../shared_drive/...`.

The previous behavior could cause issues when the lockfile was used from
different directory depths, as the relative path would resolve to the
wrong location.

This change adds a helper function `relative_to_or_absolute` that checks
if the original user input was an absolute path. If so, and if the
relative path would require traversing outside the project root (starting
with `..`), the absolute path is preserved.

Fixes #16602
This commit is contained in:
lif 2025-12-13 18:21:21 +08:00 committed by majiayu000
parent ed37f3b432
commit 74455f9066
2 changed files with 134 additions and 17 deletions

View File

@ -3598,16 +3598,26 @@ impl Source {
}
fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
let path = relative_to(&path_dist.install_path, root)
.or_else(|_| std::path::absolute(&path_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?;
// For path-based distributions, always prefer relative paths.
// These are typically local packages that should be portable.
let path = to_lockfile_path(
&path_dist.install_path,
root,
PathPreference::PreferRelative,
)
.map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Self::Path(path.into_boxed_path()))
}
fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
let path = relative_to(&path_dist.install_path, root)
.or_else(|_| std::path::absolute(&path_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?;
// For path-based distributions, always prefer relative paths.
// These are typically local packages that should be portable.
let path = to_lockfile_path(
&path_dist.install_path,
root,
PathPreference::PreferRelative,
)
.map_err(LockErrorKind::DistributionRelativePath)?;
Ok(Self::Path(path.into_boxed_path()))
}
@ -3615,9 +3625,14 @@ impl Source {
directory_dist: &DirectorySourceDist,
root: &Path,
) -> Result<Self, LockError> {
let path = relative_to(&directory_dist.install_path, root)
.or_else(|_| std::path::absolute(&directory_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?;
// For directory sources (workspace members, editable, etc.), always prefer relative paths.
// These are typically local packages that should be portable.
let path = to_lockfile_path(
&directory_dist.install_path,
root,
PathPreference::PreferRelative,
)
.map_err(LockErrorKind::DistributionRelativePath)?;
if directory_dist.editable.unwrap_or(false) {
Ok(Self::Editable(path.into_boxed_path()))
} else if directory_dist.r#virtual.unwrap_or(false) {
@ -3639,9 +3654,16 @@ impl Source {
let path = url
.to_file_path()
.map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
let path = relative_to(&path, root)
.or_else(|_| std::path::absolute(&path))
// For index URLs (find-links), always trust the user's input.
// Unlike workspace members where `given` is auto-set to resolved path,
// index URLs always come from explicit user configuration.
let preference = PathPreference::from_given_trusted(url.given());
let path = to_lockfile_path(&path, root, preference)
.map_err(LockErrorKind::IndexRelativePath)?;
// Normalize to forward slashes for cross-platform consistency.
// This is needed specifically for find-links paths on Windows where
// `to_file_path()` returns backslashes but the lockfile stores forward slashes.
let path = PathBuf::from(PortablePath::from(&path).to_string());
let source = RegistrySource::Path(path.into_boxed_path());
Ok(Self::Registry(source))
}
@ -3883,6 +3905,100 @@ impl TryFrom<SourceWire> for Source {
}
}
/// User's preference for path format in the lockfile.
///
/// When generating a lockfile, we need to decide whether to use relative or absolute paths.
/// This preference is inferred from the user's original input in the configuration file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PathPreference {
/// User explicitly specified an absolute path (e.g., `/shared/wheels`).
/// When the relative path requires upward traversal (`..`), preserve the absolute form.
PreserveAbsolute,
/// Prefer relative paths (default behavior).
PreferRelative,
}
impl PathPreference {
/// Infer path preference from user input that we know is trustworthy.
///
/// Use this for sources like `IndexUrl::Path` (find-links) where `given`
/// always comes from explicit user configuration and is never auto-generated.
///
/// If the user wrote an absolute path (e.g., `/shared/wheels` or `file:///shared/wheels`),
/// we should preserve it as absolute in the lockfile when the relative path would
/// require `..` traversal.
#[inline]
fn from_given_trusted(given: Option<&str>) -> Self {
let Some(given) = given else {
return Self::PreferRelative;
};
// Handle file:// URLs by extracting the path portion.
// - Unix: file:///path/to/dir -> /path/to/dir
// - Windows: file:///C:/path/to/dir -> C:/path/to/dir (need to strip leading /)
let path_str = if let Some(rest) = given.strip_prefix("file://") {
// On Windows, file:///C:/path becomes /C:/path after stripping file://
// We need to detect and handle this case.
#[cfg(windows)]
{
// Check if it looks like /C:/ (Windows drive letter after leading slash)
if rest.len() >= 3
&& rest.starts_with('/')
&& rest.chars().nth(1).is_some_and(|c| c.is_ascii_alphabetic())
&& rest.chars().nth(2) == Some(':')
{
&rest[1..] // Strip the leading slash to get C:/path
} else {
rest
}
}
#[cfg(not(windows))]
{
rest
}
} else {
given
};
if Path::new(path_str).is_absolute() {
Self::PreserveAbsolute
} else {
Self::PreferRelative
}
}
}
/// Convert a path to a form suitable for storing in the lockfile.
///
/// The conversion follows these rules:
/// 1. If the user explicitly specified an absolute path AND the relative path would
/// start with `..`, preserve the absolute path to ensure portability across
/// different directory depths.
/// 2. Otherwise, use a relative path when possible.
/// 3. Fall back to absolute path if relative path cannot be computed.
fn to_lockfile_path(
path: &Path,
root: &Path,
preference: PathPreference,
) -> Result<PathBuf, io::Error> {
let result = match relative_to(path, root) {
Ok(relative) => {
// Only preserve absolute path when:
// - User explicitly specified an absolute path, AND
// - Relative path requires upward traversal (starts with "..")
match preference {
PathPreference::PreserveAbsolute if relative.starts_with("..") => {
std::path::absolute(path)?
}
_ => relative,
}
}
Err(_) => std::path::absolute(path)?,
};
Ok(result)
}
/// The source for a registry, which could be a URL or a relative path.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
enum RegistrySource {

View File

@ -11114,7 +11114,7 @@ fn lock_find_links_local_wheel() -> Result<()> {
[[package]]
name = "tqdm"
version = "1000.0.0"
source = { registry = "../links" }
source = { registry = "[TEMP_DIR]/links" }
wheels = [
{ path = "tqdm-1000.0.0-py3-none-any.whl" },
]
@ -11465,7 +11465,7 @@ fn lock_find_links_local_sdist() -> Result<()> {
[[package]]
name = "tqdm"
version = "999.0.0"
source = { registry = "../links" }
source = { registry = "[TEMP_DIR]/links" }
sdist = { path = "tqdm-999.0.0.tar.gz" }
"#
);
@ -11765,7 +11765,7 @@ fn lock_find_links_explicit_index() -> Result<()> {
[[package]]
name = "tqdm"
version = "1000.0.0"
source = { registry = "../links" }
source = { registry = "[TEMP_DIR]/links" }
wheels = [
{ path = "tqdm-1000.0.0-py3-none-any.whl" },
]
@ -11775,13 +11775,14 @@ fn lock_find_links_explicit_index() -> Result<()> {
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked").current_dir(&workspace), @r"
success: true
exit_code: 0
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 2 packages in [TIME]
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
");
Ok(())
@ -11867,7 +11868,7 @@ fn lock_find_links_higher_priority_index() -> Result<()> {
[[package]]
name = "tqdm"
version = "1000.0.0"
source = { registry = "../links" }
source = { registry = "[TEMP_DIR]/links" }
wheels = [
{ path = "tqdm-1000.0.0-py3-none-any.whl" },
]