Skip to content

fix: anchor [tool.uv.sources] paths to workspace root in pixi.lock#6187

Open
jevandezande wants to merge 10 commits into
prefix-dev:mainfrom
jevandezande:fix-tool-uv-sources-relative-paths
Open

fix: anchor [tool.uv.sources] paths to workspace root in pixi.lock#6187
jevandezande wants to merge 10 commits into
prefix-dev:mainfrom
jevandezande:fix-tool-uv-sources-relative-paths

Conversation

@jevandezande
Copy link
Copy Markdown

@jevandezande jevandezande commented May 24, 2026

Description

When a path-dep's transitive deps come from another package's [tool.uv.sources], uv emits a VerbatimUrl whose given is relative to that nested package (e.g. ../pkg-b inside workspace/pkg-a). The pixi lockfile resolves relative paths against itself (the workspace root), so writing that given directly mislocates the dep after a round-trip, causing pixi install --locked to reject a lockfile that pixi install just wrote.

Fixes #4573

Root cause

When uv lowers pkg-a's requires_dist through [tool.uv.sources], it produces a Requirement whose RequirementSource::Directory carries a VerbatimUrl with two pieces:

  • url: the resolved absolute path (file:///workspace/pkg-b),
  • given: the original spelling, relative to pkg-a (../pkg-b).

Pixi was writing that given into the lockfile in two places:

  1. pkg-b's top-level location (pypi: ../pkg-b),
  2. pkg-a's requires_dist (pkg-b @ ../pkg-b).

The lockfile resolves relative paths against itself (== workspace root), so on load ../pkg-b became <workspace_parent>/pkg-b, the wrong directory. The freshly-computed satisfiability side resolved the same given against pkg-a's dir (correctly) and got file:///workspace/pkg-b. The two absolute URLs differed, so the comparison failed.

pixi install --frozen is also affected in a fresh .pixi/: it tries to install from <workspace>/../pkg-b, which doesn't exist.

Fix

Re-anchor path givens against the workspace root before serialization, via a new WorkspaceAnchor abstraction in pixi_uv_conversions:

  • WorkspaceAnchor { root: &Path } owns all lockfile-relative path logic (pathdiff,./ prefix convention, Windows backslash normalization, absolute-path preservation). It provides two call-site-appropriate methods: given_for_location (for the package's top-level location, honours "keep absolute if user wrote it absolute") and relative_given_for_file_url (for requires_dist entries, re-anchors relative and file:// givens, preserves explicit absolute paths). This mirrors the existing SourceAnchor design in pixi_spec.
  • The private process_uv_path_url helper in lock_file/resolve/pypi.rs and the private workspace_relative_given helper in pixi_uv_conversions are both deleted; their logic now lives in WorkspaceAnchor.
  • to_requirements_relative_to takes Option<&WorkspaceAnchor> instead of Option<&Path>, so the anchor travels with the context rather than being reconstructed at each call site.
  • Absolute user-specified paths (e.g. pkg = { path = "/abs/pkg" }) are now preserved uniformly in both location and requires_dist, matching the previous behavior of process_uv_path_url.

After the fix the lockfile contains pypi: ./pkg-b and pkg-b @ ./pkg-b, and round-trips cleanly.

How Has This Been Tested?

  • workspace_anchor module: 8 unit tests covering relative_path (descending/ascending), relative_given_for_file_url (non-file URL → None, absolute preserved, file:// re-anchored), and given_for_location (file:// relativized, absolute preserved, relative re-anchored).
  • conversions module: to_requirements_re_anchors_nested_tool_uv_sources_given (the bug case), to_requirements_leaves_workspace_relative_given_alone (idempotence), to_requirements_preserves_absolute_given (absolute user path survives re-anchoring).
  • lock_file/resolve/pypi module: 3 existing behavioral tests (process_uv_path_relative_path, process_uv_path_project_root_subdir, process_uv_path_absolute_path) now exercise the same logic through WorkspaceAnchor and serve as a behavioral pin for the refactor.

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.

Tools: Claude Opus 4.7 (on auto mode)

Further disclaimer, I write Python, not Rust and have never committed to Pixi previously. I tried my best to read and understand all of my code, but could very well lack the necessary context or expertise.

AI Prompt

See attached ISSUE.md file

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added sufficient tests to cover my changes

When a path-dep's transitive deps come from another package's
`[tool.uv.sources]`, uv emits a `VerbatimUrl` whose `given` is relative
to that nested package (e.g. `../pkg-b` inside `workspace/pkg-a`). The
pixi lockfile resolves relative paths against itself (the workspace
root), so writing that `given` directly mislocates the dep after a
round-trip, causing `pixi install --locked` to reject a lockfile that
`pixi install` just wrote.

Co-Authored-By: Claude <noreply@anthropic.com>
@jevandezande jevandezande force-pushed the fix-tool-uv-sources-relative-paths branch from 9cd3778 to e0d6b33 Compare May 24, 2026 02:04
@tdejager
Copy link
Copy Markdown
Contributor

tdejager commented May 24, 2026

Hey thanks for the PR! Also thanks for the additional notes regarding AI usage!

The fix looks correct: uv’s given can be relative to a nested package, while pixi.lock interprets relative paths from the workspace root, so re-anchoring before serialization makes sense.

I’d prefer not to add another standalone path helper in pixi_uv_conversions, though. This is the same kind of concept we already model with SourceAnchor: converting paths between source-relative and workspace/lockfile-relative forms. The main reason is that if relative paths are not "anchored", they lose context throughout the code, and you make mistakes later on.

Although, I'm kinda unsure we can fit that abstraction in that struct.

Could we maybe extract the “make this resolved path lockfile-relative” logic into a small shared abstraction, and use it from both:

  • process_uv_path_url
  • the new requires_dist conversion path

Something like:

struct WorkspaceAnchor<'a> {
    root: &'a Path,
}

impl WorkspaceAnchor<'_> {
    fn relative_path(&self, abs_path: &Path) -> Result<Utf8TypedPathBuf, AnchorError>;
    fn relative_given_for_file_url(&self, url: &VerbatimUrl) -> Option<String>;
}

Then to_requirements_relative_to could become either a conversion context or take this anchor, instead of owning its own pathdiff logic.

Also worth testing/documenting whether explicitly absolute user paths should stay absolute in requires_dist, since process_uv_path_url currently preserves that behavior for locations.

@baszalmstra baszalmstra requested a review from tdejager May 26, 2026 07:10
@tdejager
Copy link
Copy Markdown
Contributor

tdejager commented Jun 2, 2026

Other than the conflicts this looks good to me :)

@jevandezande
Copy link
Copy Markdown
Author

Great! What is the standard in this repo for fixing them (e.g. should I rebase on top of main and force push to this branch or merge main into this branch)?

@tdejager
Copy link
Copy Markdown
Contributor

tdejager commented Jun 3, 2026

Feel free to take any route it's squashed merged in the end :)

…elative-paths

# Conflicts:
#	Cargo.lock
#	crates/pixi_core/src/lock_file/resolve/pypi.rs
#	crates/pixi_core/src/lock_file/satisfiability/pypi.rs
#	crates/pixi_uv_conversions/src/conversions.rs
…elative-paths

# Conflicts:
#	Cargo.lock
#	crates/pixi_core/src/lock_file/resolve/pypi.rs
#	crates/pixi_core/src/lock_file/satisfiability/pypi.rs
@jevandezande
Copy link
Copy Markdown
Author

All tests are passing. This code should be ready for merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(pypi): unexpected pixi lock behavior with tool.uv.sources path dependencies

2 participants