@@ -19,6 +19,7 @@ use crate::error::{RailResult, ResultExt};
1919use crate :: progress;
2020use crate :: workspace:: WorkspaceContext ;
2121use rustc_hash:: { FxHashMap , FxHashSet } ;
22+ use semver:: Version ;
2223use semver:: VersionReq ;
2324use std:: path:: { Path , PathBuf } ;
2425use std:: sync:: Arc ;
@@ -294,6 +295,7 @@ impl UnifyAnalyzer {
294295 // Pre-compute workspace member metadata for reuse.
295296 let mut workspace_member_names: FxHashSet < Arc < str > > = FxHashSet :: default ( ) ;
296297 let mut workspace_member_paths: FxHashMap < Arc < str > , PathBuf > = FxHashMap :: default ( ) ;
298+ let mut workspace_member_versions: FxHashMap < Arc < str > , Version > = FxHashMap :: default ( ) ;
297299
298300 // Build member_paths mapping from metadata (manifest file paths)
299301 for pkg in self . metadata . workspace_packages ( ) {
@@ -304,6 +306,7 @@ impl UnifyAnalyzer {
304306 Arc :: clone ( & member_name) ,
305307 self . normalize_workspace_member_path ( & manifest_path) ,
306308 ) ;
309+ workspace_member_versions. insert ( Arc :: clone ( & member_name) , pkg. version . clone ( ) ) ;
307310 member_paths. insert ( member_name, manifest_path) ;
308311 }
309312
@@ -384,30 +387,40 @@ impl UnifyAnalyzer {
384387 // Get version from metadata - but ONLY from direct dependencies of workspace members
385388 // This uses direct_dep_versions() which filters out transitive dependencies
386389 let versions = self . metadata . direct_dep_versions ( & dep_key. name ) ;
387- if versions. is_empty ( ) {
388- continue ; // Not a direct dependency of any workspace member
389- }
390390
391- // Get unique versions across all targets
392- let unique_versions: FxHashSet < _ > = versions. values ( ) . collect ( ) ;
393-
394- // Always use highest version (cargo's resolver already picks highest compatible)
395- // When targets resolve to different versions, we unify to highest
396- let version = match unique_versions. iter ( ) . max ( ) {
397- Some ( max) if unique_versions. len ( ) > 1 => {
398- // Multiple versions found across targets - use highest
399- // This is the "silent win" - we unify duplicates automatically
400- let mut versions_found: Vec < Arc < str > > = unique_versions. iter ( ) . map ( |v| Arc :: from ( v. to_string ( ) ) ) . collect ( ) ;
401- versions_found. sort ( ) ;
402- duplicates_cleaned. push ( DuplicateCleanup {
403- dep_name : Arc :: clone ( & dep_key. name ) ,
404- versions_found,
405- selected_version : Arc :: from ( max. to_string ( ) ) ,
406- } ) ;
407- max
391+ // Workspace-member deps must be unifyable even when Cargo metadata excludes
392+ // non-default members from resolved direct-dependency traversal.
393+ let version = if versions. is_empty ( ) {
394+ if workspace_member_names. contains ( & dep_key. name ) {
395+ match workspace_member_versions. get ( & dep_key. name ) {
396+ Some ( v) => v,
397+ None => continue ,
398+ }
399+ } else {
400+ continue ; // Not a direct dependency of any workspace member
401+ }
402+ } else {
403+ // Get unique versions across all targets
404+ let unique_versions: FxHashSet < _ > = versions. values ( ) . collect ( ) ;
405+
406+ // Always use highest version (cargo's resolver already picks highest compatible)
407+ // When targets resolve to different versions, we unify to highest
408+ match unique_versions. iter ( ) . max ( ) {
409+ Some ( max) if unique_versions. len ( ) > 1 => {
410+ // Multiple versions found across targets - use highest
411+ // This is the "silent win" - we unify duplicates automatically
412+ let mut versions_found: Vec < Arc < str > > = unique_versions. iter ( ) . map ( |v| Arc :: from ( v. to_string ( ) ) ) . collect ( ) ;
413+ versions_found. sort ( ) ;
414+ duplicates_cleaned. push ( DuplicateCleanup {
415+ dep_name : Arc :: clone ( & dep_key. name ) ,
416+ versions_found,
417+ selected_version : Arc :: from ( max. to_string ( ) ) ,
418+ } ) ;
419+ * max
420+ }
421+ Some ( version) => * version,
422+ None => continue , // Empty set - shouldn't happen given versions check above
408423 }
409- Some ( version) => version,
410- None => continue , // Empty set - shouldn't happen given versions check above
411424 } ;
412425
413426 // Construct version requirement - preserve exact pins if configured
0 commit comments