Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,11 @@ impl Workspace {
.clone();
let path = normalize_path(&path);

// Canonicalize to resolve symlinks, ensuring the workspace root path is
// consistent regardless of how it's accessed (e.g. via `/symlink/to/project`
// vs `/real/path/to/project`).
let path = fs_err::canonicalize(&path).map_err(WorkspaceErrorKind::Normalize)?;

Comment on lines +319 to +323

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does dunce::canonicalize avoid the \\?\ UNC prefix issue with std::fs::canonicalize on Windows for path comparisons like starts_with?

💡 Result:

Yes, dunce::canonicalize is designed specifically to avoid the \?\ UNC (extended-length) path prefix issue on Windows that affects std::fs::canonicalize [1][2]. By default, std::fs::canonicalize on Windows returns paths with the \?\ prefix (e.g., \?\C:\Windows), which can cause compatibility issues with many Windows applications and problems when performing path comparisons like starts_with [3][4]. The dunce::canonicalize function acts as a drop-in replacement that works as follows: - It calls std::fs::canonicalize internally [5]. - On Windows, it analyzes the resulting path and, if it is safe to do so, strips the \?\ prefix to return a legacy-compatible path (e.g., C:\Windows) [1][5]. - If the path cannot be safely converted—such as when it contains reserved filenames (e.g., COM, LPT) or other characteristics that require the extended-length syntax—it leaves the UNC prefix intact to ensure the path remains valid and robust [1][2]. Because dunce::canonicalize returns the most compatible (usually non-UNC) format whenever possible, it is much safer for path comparisons like starts_with than std::fs::canonicalize, as it prevents the unexpected presence of the \?\ prefix from breaking string-based logic [1][2]. On non-Windows platforms, dunce::canonicalize simply calls the standard library function, making it safe to use cross-platform [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file around the cited lines and the cache-root comparison.
sed -n '300,360p' crates/uv-workspace/src/workspace.rs
printf '\n---\n'
sed -n '1110,1175p' crates/uv-workspace/src/workspace.rs
printf '\n---\n'
rg -n "starts_with\\(|simplified\\(|canonicalize\\(|normalize_path\\(" crates/uv-workspace/src/workspace.rs

Repository: beast-ofcourse/uv

Length of output: 6763


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the implementation of normalize_path and any Windows-specific path handling.
rg -n "fn normalize_path|normalize_path\\(" crates -g '!target'
printf '\n---\n'
fd -a normalize_path crates

Repository: beast-ofcourse/uv

Length of output: 5228


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect normalize_path and its tests for Windows verbatim-path handling.
sed -n '230,310p' crates/uv-fs/src/path.rs
printf '\n---\n'
sed -n '680,860p' crates/uv-fs/src/path.rs

Repository: beast-ofcourse/uv

Length of output: 9753


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the verbatim-path tests and simplified-path helpers.
sed -n '860,940p' crates/uv-fs/src/path.rs
printf '\n---\n'
rg -n "fn simplified|simplified_display|portable_display|from_normalized_path|\\?\\\\" crates/uv-fs/src/path.rs crates -g '!target'

Repository: beast-ofcourse/uv

Length of output: 40887


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Simplified trait implementation to see how verbatim paths are handled.
sed -n '1,90p' crates/uv-fs/src/path.rs

Repository: beast-ofcourse/uv

Length of output: 3431


Avoid comparing canonicalized and non-canonicalized cache paths on Windows.
workspace_root is canonicalized here, but external_cache_root is only normalized. On Windows, that can leave one side in \\?\ form and make starts_with miss an actual ancestor/descendant relationship, so cache-member filtering can behave incorrectly. Normalize both sides the same way before comparing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/uv-workspace/src/workspace.rs` around lines 319 - 324, The cache path
comparison is mixing a canonicalized workspace root with a merely normalized
external cache root, which can break ancestor checks on Windows. Update the path
handling in workspace_root/external_cache_root comparison logic so both values
are transformed the same way before calling starts_with, using the existing
normalization/canonicalization flow around workspace_root and
external_cache_root to keep the comparison consistent across platforms.

let project_path = path
.ancestors()
.find(|path| path.join("pyproject.toml").is_file())
Expand Down
30 changes: 30 additions & 0 deletions crates/uv/tests/workspace/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use indoc::indoc;
use insta::{assert_json_snapshot, assert_snapshot};
use serde::{Deserialize, Serialize};

use uv_fs::create_symlink;
use uv_test::{copy_dir_ignore, make_project, uv_snapshot};

fn workspaces_dir() -> PathBuf {
Expand Down Expand Up @@ -2518,3 +2519,32 @@ fn workspace_unmanaged_member_no_project() -> Result<()> {

Ok(())
}

/// Verify that workspace discovery works when the project root is reached through
/// a symlink. `Workspace::discover` canonicalizes the path early, so symlinked
/// paths resolve to the real project root.
#[test]
fn workspace_discovery_with_symlinked_root() -> Result<()> {
let context = uv_test::test_context!("3.12");

// Create a project at a real directory.
let real = context.temp_dir.join("real");
make_project(&real, "project", "dependencies = []")?;

// Create a symlink pointing to the real project.
let link = context.temp_dir.join("link");
create_symlink(&real, &link)?;

// Run `uv lock` from the symlinked path — the canonicalize fix ensures
// workspace discovery resolves symlinks and finds the project root.
uv_snapshot!(context.filters(), context.lock().current_dir(&link), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Resolved 1 package in [TIME]
");

Ok(())
}
Loading