@@ -97,6 +97,27 @@ impl UnifyAnalyzer {
9797 PathBuf :: from ( dep_path)
9898 }
9999
100+ /// Normalize a workspace member directory path relative to the workspace root.
101+ fn normalize_workspace_member_path ( & self , member_manifest_path : & Path ) -> PathBuf {
102+ let member_dir = member_manifest_path. parent ( ) . unwrap_or ( member_manifest_path) ;
103+
104+ let canonical_workspace = self
105+ . workspace_root
106+ . canonicalize ( )
107+ . unwrap_or_else ( |_| self . workspace_root . clone ( ) ) ;
108+ let canonical_member = member_dir. canonicalize ( ) . unwrap_or_else ( |_| member_dir. to_path_buf ( ) ) ;
109+
110+ if let Ok ( relative) = canonical_member. strip_prefix ( & canonical_workspace) {
111+ return relative. to_path_buf ( ) ;
112+ }
113+
114+ if let Ok ( relative) = member_dir. strip_prefix ( & self . workspace_root ) {
115+ return relative. to_path_buf ( ) ;
116+ }
117+
118+ member_dir. to_path_buf ( )
119+ }
120+
100121 /// Analyze workspace and generate unification plan
101122 pub fn analyze ( & self ) -> RailResult < UnificationPlan > {
102123 let dep_count = self . manifests . all_dependencies ( ) . len ( ) ;
@@ -107,18 +128,20 @@ impl UnifyAnalyzer {
107128 let mut duplicates_cleaned = Vec :: with_capacity ( 8 ) ;
108129 let mut version_mismatches = Vec :: with_capacity ( 8 ) ;
109130
110- // Pre-compute workspace member names for reuse
111- let workspace_member_names: FxHashSet < Arc < str > > = self
112- . metadata
113- . workspace_packages ( )
114- . iter ( )
115- . map ( |pkg| Arc :: from ( pkg. name . as_str ( ) ) )
116- . collect ( ) ;
131+ // Pre-compute workspace member metadata for reuse.
132+ let mut workspace_member_names: FxHashSet < Arc < str > > = FxHashSet :: default ( ) ;
133+ let mut workspace_member_paths: FxHashMap < Arc < str > , PathBuf > = FxHashMap :: default ( ) ;
117134
118- // Build member_paths mapping from metadata
135+ // Build member_paths mapping from metadata (manifest file paths)
119136 for pkg in self . metadata . workspace_packages ( ) {
137+ let member_name = Arc :: from ( pkg. name . as_str ( ) ) ;
120138 let manifest_path = pkg. manifest_path . clone ( ) . into_std_path_buf ( ) ;
121- member_paths. insert ( Arc :: from ( pkg. name . as_str ( ) ) , manifest_path) ;
139+ workspace_member_names. insert ( Arc :: clone ( & member_name) ) ;
140+ workspace_member_paths. insert (
141+ Arc :: clone ( & member_name) ,
142+ self . normalize_workspace_member_path ( & manifest_path) ,
143+ ) ;
144+ member_paths. insert ( member_name, manifest_path) ;
122145 }
123146
124147 progress ! ( "Analyzing {} dependencies..." , self . manifests. all_dependencies( ) . len( ) ) ;
@@ -328,20 +351,17 @@ impl UnifyAnalyzer {
328351 // Get users - use Arc<str> to share allocations
329352 let users: FxHashSet < Arc < str > > = usage_sites. iter ( ) . map ( |u| Arc :: clone ( & u. used_by ) ) . collect ( ) ;
330353
331- // Check include_paths config before processing path dependencies
332- let dep_path: Option < PathBuf > = if self . config . include_paths {
333- // Check if any usage has a path (path dependencies)
334- // If so, check if the dep is a workspace member to include path in workspace.dependencies
354+ // Workspace members must always carry `path` so member-to-member deps stay local.
355+ // This prevents dual-resolution in fresh lockfiles (local member + crates.io package).
356+ let dep_path: Option < PathBuf > = if workspace_member_names. contains ( & dep_key. name ) {
357+ workspace_member_paths. get ( & dep_key. name ) . cloned ( )
358+ } else if self . config . include_paths {
359+ // Include explicit path deps for non-member packages only when requested.
335360 usage_sites. iter ( ) . find_map ( |u| {
336361 u. path . as_ref ( ) . and_then ( |p| {
337- if !workspace_member_names. iter ( ) . any ( |m| & * * m == & * dep_key. name ) {
338- return None ;
339- }
340- // Normalize path relative to workspace root using helper
341- match & u. manifest_path {
342- Some ( manifest_path) => Some ( self . normalize_dep_path ( manifest_path, p) ) ,
343- None => Some ( PathBuf :: from ( p) ) , // No manifest_path - use as-is (shouldn't happen)
344- }
362+ u. manifest_path
363+ . as_ref ( )
364+ . map ( |manifest_path| self . normalize_dep_path ( manifest_path, p) )
345365 } )
346366 } )
347367 } else {
@@ -365,8 +385,12 @@ impl UnifyAnalyzer {
365385 existing_features. sort ( ) ;
366386 computed. sort ( ) ;
367387
368- // Also check if existing has path but we found one now
369- let path_differs = dep_path. is_some ( ) && existing. path . is_none ( ) ;
388+ // Also check if existing path is missing or differs from resolved path.
389+ let path_differs = match ( & dep_path, & existing. path ) {
390+ ( Some ( new_path) , Some ( existing_path) ) => Path :: new ( existing_path) != new_path,
391+ ( Some ( _) , None ) => true ,
392+ _ => false ,
393+ } ;
370394
371395 existing_features != computed || existing. default_features != default_features || path_differs
372396 }
0 commit comments