@@ -8,9 +8,10 @@ use anyhow::Context;
88use cargo_manifest:: Product ;
99use fs_err as fs;
1010use globwalk:: GlobWalkerBuilder ;
11- use guppy:: graph:: PackageGraph ;
11+ use guppy:: graph:: { DependencyDirection , PackageGraph } ;
1212use pathdiff:: diff_paths;
1313use serde:: { Deserialize , Serialize } ;
14+ use std:: collections:: HashSet ;
1415use std:: path:: { Path , PathBuf } ;
1516
1617#[ derive( Deserialize , Serialize , Debug , Clone , PartialEq , Eq ) ]
@@ -52,11 +53,10 @@ impl Skeleton {
5253 // Read relevant files from the filesystem
5354 let config_file = read:: config ( & base_path) ?;
5455 let mut manifests = read:: manifests ( & base_path, & graph) ?;
55- if let Some ( member) = member {
56- ignore_all_members_except ( & mut manifests, & graph, member) ;
57- }
58-
5956 let mut lock_file = read:: lockfile ( & base_path) ?;
57+ if let Some ( member) = & member {
58+ filter_to_member_closure ( & mut manifests, & mut lock_file, & graph, member) ?;
59+ }
6060 let rust_toolchain_file = read:: rust_toolchain ( & base_path) ?;
6161
6262 version_masking:: mask_local_crate_versions ( & mut manifests, & mut lock_file) ;
@@ -317,15 +317,95 @@ fn extract_package_graph(path: &Path) -> Result<PackageGraph, anyhow::Error> {
317317 cmd. build_graph ( ) . context ( "Cannot extract package graph" )
318318}
319319
320- /// If the top-level `Cargo.toml` has a `members` field, replace it with
321- /// a list consisting of just the path to the package .
320+ /// Filter the skeleton down to only the workspace members (and their transitive
321+ /// dependencies) needed to build `member` .
322322///
323- /// Also deletes the `default-members` field because it does not play nicely
324- /// with a modified `members` field and has no effect on cooking the final recipe.
325- fn ignore_all_members_except (
323+ /// Uses guppy's `query_forward` + `resolve` to compute the full transitive
324+ /// dependency closure, then:
325+ /// 1. Removes manifests for workspace members outside the closure
326+ /// 2. Updates `[workspace.members]` to list only closure members
327+ /// 3. Removes `[workspace.default-members]`
328+ /// 4. Filters the lockfile to only packages in the closure
329+ fn filter_to_member_closure (
330+ manifests : & mut Vec < ParsedManifest > ,
331+ lock_file : & mut Option < toml:: Value > ,
332+ graph : & PackageGraph ,
333+ member : & str ,
334+ ) -> Result < ( ) , anyhow:: Error > {
335+ let ws = graph. workspace ( ) ;
336+ // `member` may be a package name or a binary target name (from --bin).
337+ // Try package name first, then search for a binary target.
338+ let pkg = match ws. member_by_name ( member) {
339+ Ok ( pkg) => pkg,
340+ Err ( _) => ws
341+ . iter ( )
342+ . find ( |pkg| {
343+ pkg. build_targets ( ) . any ( |t| {
344+ matches ! ( t. id( ) , guppy:: graph:: BuildTargetId :: Binary ( name) if name == member)
345+ } )
346+ } )
347+ . ok_or_else ( || {
348+ anyhow:: anyhow!( "No workspace package or binary target named '{member}'" )
349+ } ) ?,
350+ } ;
351+
352+ // Get transitive closure of all dependencies
353+ let resolved = graph. query_forward ( std:: iter:: once ( pkg. id ( ) ) ) ?. resolve ( ) ;
354+
355+ // Collect workspace member names in the closure
356+ let closure_members: HashSet < String > = ws
357+ . iter ( )
358+ . filter ( |ws_pkg| resolved. contains ( ws_pkg. id ( ) ) . unwrap_or ( false ) )
359+ . map ( |ws_pkg| ws_pkg. name ( ) . to_string ( ) )
360+ . collect ( ) ;
361+
362+ // 1. Filter manifests: keep root workspace manifest + closure members
363+ manifests. retain ( |m| {
364+ extract_pkg_name ( & m. contents ) . is_none_or ( |name| closure_members. contains ( & name) )
365+ } ) ;
366+
367+ // 2. Collect workspace dep keys referenced by closure members
368+ let referenced_workspace_deps = collect_workspace_dep_keys ( manifests) ;
369+
370+ // 3. Update [workspace.members], remove default-members, filter [workspace.dependencies]
371+ update_workspace_members (
372+ manifests,
373+ graph,
374+ & closure_members,
375+ & referenced_workspace_deps,
376+ ) ;
377+
378+ // 4. Collect ALL (name, version) pairs in the closure (workspace + external)
379+ let closure_packages: HashSet < ( String , String ) > = resolved
380+ . packages ( DependencyDirection :: Forward )
381+ . map ( |pkg| ( pkg. name ( ) . to_string ( ) , pkg. version ( ) . to_string ( ) ) )
382+ . collect ( ) ;
383+
384+ // 5. Filter lockfile: keep only packages in the closure
385+ if let Some ( lockfile) = lock_file {
386+ filter_lockfile_packages ( lockfile, & closure_packages) ;
387+ }
388+
389+ Ok ( ( ) )
390+ }
391+
392+ /// Extract the `[package].name` from a parsed TOML manifest, if present.
393+ fn extract_pkg_name ( contents : & toml:: Value ) -> Option < String > {
394+ contents
395+ . get ( "package" ) ?
396+ . get ( "name" ) ?
397+ . as_str ( )
398+ . map ( |s| s. to_string ( ) )
399+ }
400+
401+ /// Replace `[workspace.members]` with relative paths for all closure members,
402+ /// remove `[workspace.default-members]`, and filter `[workspace.dependencies]`
403+ /// to only those referenced by closure members.
404+ fn update_workspace_members (
326405 manifests : & mut [ ParsedManifest ] ,
327406 graph : & PackageGraph ,
328- member : String ,
407+ closure_members : & HashSet < String > ,
408+ referenced_workspace_deps : & HashSet < String > ,
329409) {
330410 let workspace_toml = manifests
331411 . iter_mut ( )
@@ -336,22 +416,78 @@ fn ignore_all_members_except(
336416 let ws = graph. workspace ( ) ;
337417 let workspace_root = ws. root ( ) ;
338418
339- if let Ok ( pkg) = ws. member_by_name ( & member) {
340- // Make this a relative path to the workspace, and remove the `Cargo.toml` child.
341- let member_cargo_path = diff_paths ( pkg. manifest_path ( ) , workspace_root) ;
342- let member_workspace_path = member_cargo_path
343- . as_ref ( )
344- . and_then ( |path| path. parent ( ) )
345- . and_then ( |dir| dir. to_str ( ) ) ;
346-
347- if let Some ( member_path) = member_workspace_path {
348- * members =
349- toml:: Value :: Array ( vec ! [ toml:: Value :: String ( member_path. to_string( ) ) ] ) ;
350- }
351- }
419+ let member_paths: Vec < toml:: Value > = ws
420+ . iter ( )
421+ . filter ( |pkg| closure_members. contains ( pkg. name ( ) ) )
422+ . filter_map ( |pkg| {
423+ let cargo_path = diff_paths ( pkg. manifest_path ( ) , workspace_root) ?;
424+ let dir = cargo_path. parent ( ) ?;
425+ Some ( toml:: Value :: String ( dir. to_str ( ) ?. to_string ( ) ) )
426+ } )
427+ . collect ( ) ;
428+
429+ * members = toml:: Value :: Array ( member_paths) ;
352430 }
353431 if let Some ( workspace) = workspace. as_table_mut ( ) {
354432 workspace. remove ( "default-members" ) ;
433+ if let Some ( deps) = workspace
434+ . get_mut ( "dependencies" )
435+ . and_then ( |d| d. as_table_mut ( ) )
436+ {
437+ deps. retain ( |key, _| referenced_workspace_deps. contains ( key) ) ;
438+ }
439+ }
440+ }
441+ }
442+
443+ /// Collect all `[workspace.dependencies]` keys referenced (via `workspace = true`)
444+ /// by the given manifests.
445+ fn collect_workspace_dep_keys ( manifests : & [ ParsedManifest ] ) -> HashSet < String > {
446+ let mut keys = HashSet :: new ( ) ;
447+ for manifest in manifests {
448+ if extract_pkg_name ( & manifest. contents ) . is_none ( ) {
449+ continue ; // skip root workspace manifest
450+ }
451+ collect_workspace_keys_from ( & manifest. contents , & mut keys) ;
452+ if let Some ( targets) = manifest. contents . get ( "target" ) . and_then ( |t| t. as_table ( ) ) {
453+ for ( _, target_config) in targets {
454+ collect_workspace_keys_from ( target_config, & mut keys) ;
455+ }
355456 }
356457 }
458+ keys
459+ }
460+
461+ fn collect_workspace_keys_from ( value : & toml:: Value , keys : & mut HashSet < String > ) {
462+ for section in [ "dependencies" , "dev-dependencies" , "build-dependencies" ] {
463+ if let Some ( deps) = value. get ( section) . and_then ( |d| d. as_table ( ) ) {
464+ for ( key, dep) in deps {
465+ if dep
466+ . get ( "workspace" )
467+ . and_then ( |w| w. as_bool ( ) )
468+ . unwrap_or ( false )
469+ {
470+ keys. insert ( key. clone ( ) ) ;
471+ }
472+ }
473+ }
474+ }
475+ }
476+
477+ /// Retain only lockfile `[[package]]` entries whose `(name, version)` pair
478+ /// is in the resolved closure.
479+ fn filter_lockfile_packages (
480+ lockfile : & mut toml:: Value ,
481+ closure_packages : & HashSet < ( String , String ) > ,
482+ ) {
483+ if let Some ( packages) = lockfile. get_mut ( "package" ) . and_then ( |p| p. as_array_mut ( ) ) {
484+ packages. retain ( |pkg| {
485+ let name = pkg. get ( "name" ) . and_then ( |n| n. as_str ( ) ) ;
486+ let version = pkg. get ( "version" ) . and_then ( |v| v. as_str ( ) ) ;
487+ match ( name, version) {
488+ ( Some ( n) , Some ( v) ) => closure_packages. contains ( & ( n. to_string ( ) , v. to_string ( ) ) ) ,
489+ _ => true ,
490+ }
491+ } ) ;
492+ }
357493}
0 commit comments