Skip to content
Closed
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
62 changes: 55 additions & 7 deletions crates/pixi_record/src/pinned_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,18 @@ impl PinnedSourceSpec {

// Normalize the path (resolve . and ..)
let normalized = crate::path_utils::normalize_path(&target_path_abs);
// Convert back to a path that's either absolute or relative to workspace
let path_spec = normalized.strip_prefix(workspace_root).expect(
"the workspace_root should be part of the source build path at this point",
);

Some(PinnedSourceSpec::Path(PinnedPathSpec {
path: Utf8TypedPathBuf::from(path_spec.to_string_lossy().as_ref()),
}))
// Try to make the path relative to workspace_root if it's within the workspace
// Otherwise, keep it as absolute (to avoid panic for paths outside workspace)
let path = if let Ok(relative) = normalized.strip_prefix(workspace_root) {
// Path is within workspace, make it relative
Utf8TypedPathBuf::from(relative.to_string_lossy().as_ref())
} else {
// Path is outside workspace, keep it absolute
Utf8TypedPathBuf::from(normalized.to_string_lossy().as_ref())
};

Some(PinnedSourceSpec::Path(PinnedPathSpec { path }))
}

// Git-to-Git: If same repository, convert relative path to subdirectory
Expand Down Expand Up @@ -1040,6 +1044,7 @@ mod tests {

use pixi_git::sha::GitSha;
use pixi_spec::{GitReference, GitSpec};
use typed_path::Utf8UnixPathBuf;
use url::Url;

use crate::{PinnedGitCheckout, PinnedGitSpec, PinnedUrlSpec, SourceMismatchError};
Expand Down Expand Up @@ -1677,4 +1682,47 @@ mod tests {
// From /foo/baz/quux to /foo/bar/qux requires ../../bar/qux
assert_eq!(path.as_str(), "../../bar/qux");
}

/// Regression test for #5178: from_relative_to should handle paths outside workspace
///
/// This test verifies that when converting a relative path from a lock file back
/// to a full pinned source spec, paths that resolve outside the workspace_root
/// don't cause a panic.
///
/// The bug was that the old code tried to strip workspace_root prefix from all
/// normalized paths with `.expect()`, which would panic when the path was outside
/// the workspace.
///
/// Scenario: A build_source in a sibling directory outside the workspace
/// - workspace is at /workspace/project
/// - manifest_source is at /workspace/project/manifest
/// - build_source is at /workspace/alternative-source (outside project)
/// - relative path from manifest to build: ../../alternative-source
#[test]
fn test_from_relative_to_with_out_of_workspace_path() {
let workspace_root = Path::new("/workspace/project");

// Base (manifest_source) is relative to workspace
let base_spec = PinnedSourceSpec::Path(PinnedPathSpec {
path: "manifest".into(), // Resolves to /workspace/project/manifest
});

// build_source path relative to manifest_source points outside workspace
let relative_path = Utf8UnixPathBuf::from("../../alternative-source");

// This should NOT panic - it should return the normalized absolute path
let result = PinnedSourceSpec::from_relative_to(relative_path, &base_spec, workspace_root);

let result_spec = result.expect("Should return Some for valid relative path");

// Verify the result is a path spec with absolute path outside workspace
let path_spec = result_spec.as_path().expect("Should be a path spec");
let resolved = path_spec.resolve(workspace_root);

assert!(
resolved.ends_with("alternative-source"),
"Resolved path should end with 'alternative-source', got: {}",
resolved.display()
);
}
}
Loading