@@ -26,6 +26,12 @@ pub(crate) enum InstallTarget<'lock> {
2626 name : & ' lock PackageName ,
2727 lock : & ' lock Lock ,
2828 } ,
29+ /// Multiple specific projects in a workspace.
30+ Projects {
31+ workspace : & ' lock Workspace ,
32+ names : & ' lock [ PackageName ] ,
33+ lock : & ' lock Lock ,
34+ } ,
2935 /// An entire workspace.
3036 Workspace {
3137 workspace : & ' lock Workspace ,
@@ -47,6 +53,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
4753 fn install_path ( & self ) -> & ' lock Path {
4854 match self {
4955 Self :: Project { workspace, .. } => workspace. install_path ( ) ,
56+ Self :: Projects { workspace, .. } => workspace. install_path ( ) ,
5057 Self :: Workspace { workspace, .. } => workspace. install_path ( ) ,
5158 Self :: NonProjectWorkspace { workspace, .. } => workspace. install_path ( ) ,
5259 Self :: Script { script, .. } => script. path . parent ( ) . unwrap ( ) ,
@@ -56,36 +63,40 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
5663 fn lock ( & self ) -> & ' lock Lock {
5764 match self {
5865 Self :: Project { lock, .. } => lock,
66+ Self :: Projects { lock, .. } => lock,
5967 Self :: Workspace { lock, .. } => lock,
6068 Self :: NonProjectWorkspace { lock, .. } => lock,
6169 Self :: Script { lock, .. } => lock,
6270 }
6371 }
6472
65- fn roots ( & self ) -> impl Iterator < Item = & PackageName > {
73+ #[ allow( refining_impl_trait) ]
74+ fn roots ( & self ) -> Box < dyn Iterator < Item = & PackageName > + ' _ > {
6675 match self {
67- Self :: Project { name, .. } => Either :: Left ( Either :: Left ( std:: iter:: once ( * name) ) ) ,
76+ Self :: Project { name, .. } => Box :: new ( std:: iter:: once ( * name) ) ,
77+ Self :: Projects { names, .. } => Box :: new ( names. iter ( ) ) ,
6878 Self :: NonProjectWorkspace { lock, .. } => {
69- Either :: Left ( Either :: Right ( lock. members ( ) . iter ( ) ) )
79+ Box :: new ( lock. members ( ) . iter ( ) )
7080 }
7181 Self :: Workspace { lock, .. } => {
7282 // Identify the workspace members.
7383 //
7484 // The members are encoded directly in the lockfile, unless the workspace contains a
7585 // single member at the root, in which case, we identify it by its source.
7686 if lock. members ( ) . is_empty ( ) {
77- Either :: Right ( Either :: Left ( lock. root ( ) . into_iter ( ) . map ( Package :: name) ) )
87+ Box :: new ( lock. root ( ) . into_iter ( ) . map ( Package :: name) )
7888 } else {
79- Either :: Left ( Either :: Right ( lock. members ( ) . iter ( ) ) )
89+ Box :: new ( lock. members ( ) . iter ( ) )
8090 }
8191 }
82- Self :: Script { .. } => Either :: Right ( Either :: Right ( std:: iter:: empty ( ) ) ) ,
92+ Self :: Script { .. } => Box :: new ( std:: iter:: empty ( ) ) ,
8393 }
8494 }
8595
8696 fn project_name ( & self ) -> Option < & PackageName > {
8797 match self {
8898 Self :: Project { name, .. } => Some ( name) ,
99+ Self :: Projects { .. } => None ,
89100 Self :: Workspace { .. } => None ,
90101 Self :: NonProjectWorkspace { .. } => None ,
91102 Self :: Script { .. } => None ,
@@ -98,6 +109,7 @@ impl<'lock> InstallTarget<'lock> {
98109 pub ( crate ) fn indexes ( self ) -> impl Iterator < Item = & ' lock Index > {
99110 match self {
100111 Self :: Project { workspace, .. }
112+ | Self :: Projects { workspace, .. }
101113 | Self :: Workspace { workspace, .. }
102114 | Self :: NonProjectWorkspace { workspace, .. } => {
103115 Either :: Left ( workspace. indexes ( ) . iter ( ) . chain (
@@ -130,6 +142,7 @@ impl<'lock> InstallTarget<'lock> {
130142 pub ( crate ) fn sources ( & self ) -> impl Iterator < Item = & Source > {
131143 match self {
132144 Self :: Project { workspace, .. }
145+ | Self :: Projects { workspace, .. }
133146 | Self :: Workspace { workspace, .. }
134147 | Self :: NonProjectWorkspace { workspace, .. } => {
135148 Either :: Left ( workspace. sources ( ) . values ( ) . flat_map ( Sources :: iter) . chain (
@@ -158,6 +171,7 @@ impl<'lock> InstallTarget<'lock> {
158171 ) -> impl Iterator < Item = Cow < ' lock , uv_pep508:: Requirement < VerbatimParsedUrl > > > {
159172 match self {
160173 Self :: Project { workspace, .. }
174+ | Self :: Projects { workspace, .. }
161175 | Self :: Workspace { workspace, .. }
162176 | Self :: NonProjectWorkspace { workspace, .. } => {
163177 Either :: Left (
@@ -256,6 +270,7 @@ impl<'lock> InstallTarget<'lock> {
256270 }
257271 match self {
258272 Self :: Project { lock, .. }
273+ | Self :: Projects { lock, .. }
259274 | Self :: Workspace { lock, .. }
260275 | Self :: NonProjectWorkspace { lock, .. } => {
261276 if !lock. supports_provides_extra ( ) {
@@ -281,6 +296,9 @@ impl<'lock> InstallTarget<'lock> {
281296 Self :: Project { .. } => {
282297 Err ( ProjectError :: MissingExtraProject ( extra. clone ( ) ) )
283298 }
299+ Self :: Projects { .. } => {
300+ Err ( ProjectError :: MissingExtraWorkspace ( extra. clone ( ) ) )
301+ }
284302 _ => Err ( ProjectError :: MissingExtraWorkspace ( extra. clone ( ) ) ) ,
285303 } ;
286304 }
@@ -341,23 +359,27 @@ impl<'lock> InstallTarget<'lock> {
341359 }
342360 }
343361 }
344- Self :: Project { lock, .. } => {
362+ Self :: Project { lock, .. } | Self :: Projects { lock , .. } => {
345363 let roots = self . roots ( ) . collect :: < FxHashSet < _ > > ( ) ;
346364 let member_packages: Vec < & Package > = lock
347365 . packages ( )
348366 . iter ( )
349367 . filter ( |package| roots. contains ( package. name ( ) ) )
350368 . collect ( ) ;
351369
352- // Extract the dependency groups defined in the relevant member.
370+ // Extract the dependency groups defined in the relevant member(s) .
353371 let known_groups = member_packages
354372 . iter ( )
355373 . flat_map ( |package| package. dependency_groups ( ) . keys ( ) )
356374 . collect :: < FxHashSet < _ > > ( ) ;
357375
358376 for group in groups. explicit_names ( ) {
359377 if !known_groups. contains ( group) {
360- return Err ( ProjectError :: MissingGroupProject ( group. clone ( ) ) ) ;
378+ return match self {
379+ Self :: Project { .. } => Err ( ProjectError :: MissingGroupProject ( group. clone ( ) ) ) ,
380+ Self :: Projects { .. } => Err ( ProjectError :: MissingGroupWorkspace ( group. clone ( ) ) ) ,
381+ _ => unreachable ! ( ) ,
382+ } ;
361383 }
362384 }
363385 }
@@ -381,7 +403,7 @@ impl<'lock> InstallTarget<'lock> {
381403 ) -> BTreeSet < & PackageName > {
382404 match self {
383405 Self :: Project { name, lock, .. } => {
384- // Collect the packages by name for efficient lookup
406+ // Collect the packages by name for efficient lookup.
385407 let packages = lock
386408 . packages ( )
387409 . iter ( )
@@ -463,6 +485,98 @@ impl<'lock> InstallTarget<'lock> {
463485
464486 required_members
465487 }
488+ Self :: Projects { names, lock, .. } => {
489+ // Collect the packages by name for efficient lookup.
490+ let packages = lock
491+ . packages ( )
492+ . iter ( )
493+ . map ( |p| ( p. name ( ) , p) )
494+ . collect :: < BTreeMap < _ , _ > > ( ) ;
495+
496+ // We'll include all specified projects
497+ let mut required_members = BTreeSet :: new ( ) ;
498+ for name in names. iter ( ) {
499+ required_members. insert ( name) ;
500+ }
501+
502+ // Find all workspace member dependencies recursively for all specified packages
503+ let mut queue: VecDeque < ( & PackageName , Option < & ExtraName > ) > = VecDeque :: new ( ) ;
504+ let mut seen: FxHashSet < ( & PackageName , Option < & ExtraName > ) > = FxHashSet :: default ( ) ;
505+
506+ for name in names. iter ( ) {
507+ let Some ( root_package) = packages. get ( name) else {
508+ continue ;
509+ } ;
510+
511+ if groups. prod ( ) {
512+ // Add the root package
513+ if seen. insert ( ( name, None ) ) {
514+ queue. push_back ( ( name, None ) ) ;
515+ }
516+
517+ // Add explicitly activated extras for the root package
518+ for extra in extras. extra_names ( root_package. optional_dependencies ( ) . keys ( ) ) {
519+ if seen. insert ( ( name, Some ( extra) ) ) {
520+ queue. push_back ( ( name, Some ( extra) ) ) ;
521+ }
522+ }
523+ }
524+
525+ // Add activated dependency groups for the root package
526+ for ( group_name, dependencies) in root_package. resolved_dependency_groups ( ) {
527+ if !groups. contains ( group_name) {
528+ continue ;
529+ }
530+ for dependency in dependencies {
531+ let dep_name = dependency. package_name ( ) ;
532+ if seen. insert ( ( dep_name, None ) ) {
533+ queue. push_back ( ( dep_name, None ) ) ;
534+ }
535+ for extra in dependency. extra ( ) {
536+ if seen. insert ( ( dep_name, Some ( extra) ) ) {
537+ queue. push_back ( ( dep_name, Some ( extra) ) ) ;
538+ }
539+ }
540+ }
541+ }
542+ }
543+
544+ while let Some ( ( pkg_name, extra) ) = queue. pop_front ( ) {
545+ if lock. members ( ) . contains ( pkg_name) {
546+ required_members. insert ( pkg_name) ;
547+ }
548+
549+ let Some ( package) = packages. get ( pkg_name) else {
550+ continue ;
551+ } ;
552+
553+ let Some ( dependencies) = extra
554+ . map ( |extra_name| {
555+ package
556+ . optional_dependencies ( )
557+ . get ( extra_name)
558+ . map ( Vec :: as_slice)
559+ } )
560+ . unwrap_or ( Some ( package. dependencies ( ) ) )
561+ else {
562+ continue ;
563+ } ;
564+
565+ for dependency in dependencies {
566+ let name = dependency. package_name ( ) ;
567+ if seen. insert ( ( name, None ) ) {
568+ queue. push_back ( ( name, None ) ) ;
569+ }
570+ for extra in dependency. extra ( ) {
571+ if seen. insert ( ( name, Some ( extra) ) ) {
572+ queue. push_back ( ( name, Some ( extra) ) ) ;
573+ }
574+ }
575+ }
576+ }
577+
578+ required_members
579+ }
466580 Self :: Workspace { lock, .. } | Self :: NonProjectWorkspace { lock, .. } => {
467581 // Return all workspace members
468582 lock. members ( ) . iter ( ) . collect ( )
0 commit comments