mirror of https://github.com/astral-sh/uv
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:
parent
ed37f3b432
commit
74455f9066
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue