11#![ allow( clippy:: missing_errors_doc) ]
22#![ warn( missing_docs) ]
33
4+ //! Run cargo commands for all feature combinations across a workspace.
5+ //!
6+ //! This crate powers the `cargo-fc` and `cargo-feature-combinations` binaries.
7+ //! The main entry point for consumers is [`run`], which parses CLI arguments
8+ //! and dispatches the requested command.
9+
410mod config;
511mod tee;
612
@@ -23,6 +29,7 @@ static RED: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Red, true))
2329static YELLOW : LazyLock < ColorSpec > = LazyLock :: new ( || color_spec ( Color :: Yellow , true ) ) ;
2430static GREEN : LazyLock < ColorSpec > = LazyLock :: new ( || color_spec ( Color :: Green , true ) ) ;
2531
32+ /// Summary of the outcome for running a cargo command on a single feature set.
2633#[ derive( Debug ) ]
2734pub struct Summary {
2835 package_name : String ,
@@ -33,31 +40,64 @@ pub struct Summary {
3340 num_errors : usize ,
3441}
3542
43+ /// High-level command requested by the user.
3644#[ derive( Debug ) ]
3745pub enum Command {
38- FeatureMatrix { pretty : bool } ,
46+ /// Print a JSON feature matrix to stdout.
47+ ///
48+ /// The matrix is produced by combining [`Package::feature_matrix`] for all
49+ /// selected packages into a single JSON array.
50+ FeatureMatrix {
51+ /// Whether to pretty-print the JSON feature matrix.
52+ pretty : bool ,
53+ } ,
54+ /// Print the tool version and exit.
3955 Version ,
56+ /// Print help text and exit.
4057 Help ,
4158}
4259
60+ /// Command-line options recognized by this crate.
61+ ///
62+ /// Instances of this type are produced by [`parse_arguments`] and consumed by
63+ /// [`run`] to drive command selection and filtering.
4364#[ derive( Debug , Default ) ]
4465#[ allow( clippy:: struct_excessive_bools) ]
4566pub struct Options {
67+ /// Optional path to the Cargo manifest that should be inspected.
4668 pub manifest_path : Option < PathBuf > ,
69+ /// Explicit list of package names to include.
4770 pub packages : HashSet < String > ,
71+ /// List of package names to exclude.
4872 pub exclude_packages : HashSet < String > ,
73+ /// High-level command to execute.
4974 pub command : Option < Command > ,
75+ /// Whether to restrict processing to packages with a library target.
5076 pub only_packages_with_lib_target : bool ,
77+ /// Whether to hide cargo output and only show the final summary.
5178 pub silent : bool ,
79+ /// Whether to print more verbose information such as the full cargo command.
5280 pub verbose : bool ,
81+ /// Whether to treat warnings like errors for the summary and `--fail-fast`.
5382 pub pedantic : bool ,
83+ /// Whether to silence warnings from rustc and only show errors.
5484 pub errors_only : bool ,
85+ /// Whether to only list packages instead of all feature combinations.
5586 pub packages_only : bool ,
87+ /// Whether to stop processing after the first failing feature combination.
5688 pub fail_fast : bool ,
5789}
5890
91+ /// Helper trait to provide simple argument parsing over `Vec<String>`.
5992pub trait ArgumentParser {
93+ /// Check whether an argument flag exists, either as a standalone flag or
94+ /// in `--flag=value` form.
6095 fn contains ( & self , arg : & str ) -> bool ;
96+ /// Extract all occurrences of an argument and their values.
97+ ///
98+ /// When `has_value` is `true`, this matches `--flag value` and
99+ /// `--flag=value` forms and returns the value part. When `has_value` is
100+ /// `false`, it matches bare flags like `--flag`.
61101 fn get_all ( & self , arg : & str , has_value : bool )
62102 -> Vec < ( std:: ops:: RangeInclusive < usize > , String ) > ;
63103}
@@ -94,11 +134,12 @@ impl ArgumentParser for Vec<String> {
94134 }
95135}
96136
137+ /// Abstraction over a Cargo workspace used by this crate.
97138pub trait Workspace {
98- /// Returns the workspace configuration section for feature combinations.
139+ /// Return the workspace configuration section for feature combinations.
99140 fn workspace_config ( & self ) -> eyre:: Result < WorkspaceConfig > ;
100141
101- /// Returns the packages relevant for feature combinations.
142+ /// Return the packages that should be considered for feature combinations.
102143 fn packages_for_fc ( & self ) -> eyre:: Result < Vec < & cargo_metadata:: Package > > ;
103144}
104145
@@ -188,8 +229,9 @@ impl Workspace for cargo_metadata::Metadata {
188229 }
189230}
190231
232+ /// Extension trait for [`cargo_metadata::Package`] used by this crate.
191233pub trait Package {
192- /// Parses the config for this package if present.
234+ /// Parse the configuration for this package if present.
193235 ///
194236 /// If the Cargo.toml manifest contains a configuration section,
195237 /// the latter is parsed.
@@ -198,10 +240,14 @@ pub trait Package {
198240 /// # Errors
199241 ///
200242 /// If the configuration in the manifest can not be parsed,
201- /// an Error is returned.
243+ /// an error is returned.
202244 ///
203245 fn config ( & self ) -> eyre:: Result < Config > ;
246+ /// Compute all feature combinations for this package based on the
247+ /// provided [`Config`].
204248 fn feature_combinations < ' a > ( & ' a self , config : & ' a Config ) -> Vec < Vec < & ' a String > > ;
249+ /// Convert [`Package::feature_combinations`] into a list of comma-separated
250+ /// feature strings suitable for passing to `cargo --features`.
205251 fn feature_matrix ( & self , config : & Config ) -> Vec < String > ;
206252}
207253
@@ -371,6 +417,16 @@ fn generate_isolated_base_powerset<'a>(
371417 . collect ( )
372418}
373419
420+ /// Print a JSON feature matrix for the given packages to stdout.
421+ ///
422+ /// The matrix is a JSON array of objects produced from each package's
423+ /// configuration and the feature combinations returned by
424+ /// [`Package::feature_matrix`].
425+ ///
426+ /// # Errors
427+ ///
428+ /// Returns an error if any configuration can not be parsed or serialization
429+ /// of the JSON matrix fails.
374430pub fn print_feature_matrix (
375431 packages : & [ & cargo_metadata:: Package ] ,
376432 pretty : bool ,
@@ -414,6 +470,7 @@ pub fn print_feature_matrix(
414470 Ok ( ( ) )
415471}
416472
473+ /// Build a [`ColorSpec`] with the given foreground color and bold setting.
417474#[ must_use]
418475pub fn color_spec ( color : Color , bold : bool ) -> ColorSpec {
419476 let mut spec = ColorSpec :: new ( ) ;
@@ -422,6 +479,10 @@ pub fn color_spec(color: Color, bold: bool) -> ColorSpec {
422479 spec
423480}
424481
482+ /// Extract per-crate warning counts from cargo output.
483+ ///
484+ /// The iterator yields the number of warnings for each compiled crate that
485+ /// matches the summary line produced by cargo.
425486pub fn warning_counts ( output : & str ) -> impl Iterator < Item = usize > + ' _ {
426487 static WARNING_REGEX : LazyLock < Regex > =
427488 LazyLock :: new ( || Regex :: new ( r"warning: .* generated (\d+) warnings?" ) . unwrap ( ) ) ;
@@ -431,6 +492,10 @@ pub fn warning_counts(output: &str) -> impl Iterator<Item = usize> + '_ {
431492 . map ( |m| m. as_str ( ) . parse :: < usize > ( ) . unwrap_or ( 0 ) )
432493}
433494
495+ /// Extract per-crate error counts from cargo output.
496+ ///
497+ /// The iterator yields the number of errors for each compiled crate that
498+ /// matches the summary line produced by cargo.
434499pub fn error_counts ( output : & str ) -> impl Iterator < Item = usize > + ' _ {
435500 static ERROR_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
436501 Regex :: new ( r"error: could not compile `.*` due to\s*(\d*)\s*previous errors?" ) . unwrap ( )
@@ -441,6 +506,10 @@ pub fn error_counts(output: &str) -> impl Iterator<Item = usize> + '_ {
441506 . map ( |m| m. as_str ( ) . parse :: < usize > ( ) . unwrap_or ( 1 ) )
442507}
443508
509+ /// Print an aggregated summary for all executed feature combinations.
510+ ///
511+ /// This function is used by [`run_cargo_command`] after all packages and
512+ /// feature sets have been processed.
444513pub fn print_summary (
445514 summary : Vec < Summary > ,
446515 mut stdout : termcolor:: StandardStream ,
@@ -553,6 +622,15 @@ fn print_package_cmd(
553622 }
554623}
555624
625+ /// Run a cargo command for all requested packages and feature combinations.
626+ ///
627+ /// This function drives the main execution loop by spawning cargo for each
628+ /// feature set and collecting a [`Summary`] for every run.
629+ ///
630+ /// # Errors
631+ ///
632+ /// Returns an error if a cargo process can not be spawned or if IO operations
633+ /// fail while reading cargo's output.
556634pub fn run_cargo_command (
557635 packages : & [ & cargo_metadata:: Package ] ,
558636 mut cargo_args : Vec < & str > ,
@@ -763,6 +841,7 @@ enum CargoSubcommand {
763841 Other ,
764842}
765843
844+ /// Determine the cargo subcommand implied by the argument list.
766845fn cargo_subcommand ( args : & [ impl AsRef < str > ] ) -> CargoSubcommand {
767846 let args: HashSet < & str > = args. iter ( ) . map ( AsRef :: as_ref) . collect ( ) ;
768847 if args. contains ( "build" ) || args. contains ( "b" ) {
@@ -780,6 +859,15 @@ fn cargo_subcommand(args: &[impl AsRef<str>]) -> CargoSubcommand {
780859 }
781860}
782861
862+ /// Parse command-line arguments for the `cargo-*` binary.
863+ ///
864+ /// The returned [`Options`] drives workspace discovery and filtering, while
865+ /// the remaining `Vec<String>` contains the raw cargo arguments.
866+ ///
867+ /// # Errors
868+ ///
869+ /// Returns an error if the manifest path passed via `--manifest-path` does
870+ /// not exist or can not be canonicalized.
783871pub fn parse_arguments ( bin_name : & str ) -> eyre:: Result < ( Options , Vec < String > ) > {
784872 let mut args: Vec < String > = std:: env:: args_os ( )
785873 // Skip executable name
@@ -894,6 +982,14 @@ pub fn parse_arguments(bin_name: &str) -> eyre::Result<(Options, Vec<String>)> {
894982 Ok ( ( options, args) )
895983}
896984
985+ /// Run the cargo subcommand for all relevant feature combinations.
986+ ///
987+ /// This is the main entry point used by the binaries in this crate.
988+ ///
989+ /// # Errors
990+ ///
991+ /// Returns an error if argument parsing fails or `cargo metadata` can not be
992+ /// executed successfully.
897993pub fn run ( bin_name : & str ) -> eyre:: Result < ( ) > {
898994 color_eyre:: install ( ) ?;
899995
@@ -989,7 +1085,7 @@ mod test {
9891085 init ( ) ;
9901086 let package = package_with_features ( & [ "foo-c" , "foo-a" , "foo-b" ] ) ?;
9911087 let config = Config :: default ( ) ;
992- let expected_combinations = vec ! [
1088+ let want = vec ! [
9931089 vec![ ] ,
9941090 vec![ "foo-a" ] ,
9951091 vec![ "foo-a" , "foo-b" ] ,
@@ -999,10 +1095,9 @@ mod test {
9991095 vec![ "foo-b" , "foo-c" ] ,
10001096 vec![ "foo-c" ] ,
10011097 ] ;
1098+ let have = package. feature_combinations ( & config) ;
10021099
1003- let actual_combinations = package. feature_combinations ( & config) ;
1004-
1005- sim_assert_eq ! ( expected_combinations, actual_combinations) ;
1100+ sim_assert_eq ! ( have: have, want: want) ;
10061101 Ok ( ( ) )
10071102 }
10081103
@@ -1018,7 +1113,7 @@ mod test {
10181113 ] ,
10191114 ..Default :: default ( )
10201115 } ;
1021- let expected_combinations = vec ! [
1116+ let want = vec ! [
10221117 vec![ ] ,
10231118 vec![ "bar-a" ] ,
10241119 vec![ "bar-a" , "bar-b" ] ,
@@ -1027,10 +1122,9 @@ mod test {
10271122 vec![ "foo-a" , "foo-b" ] ,
10281123 vec![ "foo-b" ] ,
10291124 ] ;
1125+ let have = package. feature_combinations ( & config) ;
10301126
1031- let actual_combinations = package. feature_combinations ( & config) ;
1032-
1033- sim_assert_eq ! ( expected_combinations, actual_combinations) ;
1127+ sim_assert_eq ! ( have: have, want: want) ;
10341128 Ok ( ( ) )
10351129 }
10361130
@@ -1046,17 +1140,16 @@ mod test {
10461140 ] ,
10471141 ..Default :: default ( )
10481142 } ;
1049- let expected_combinations = vec ! [
1143+ let want = vec ! [
10501144 vec![ ] ,
10511145 vec![ "bar-a" ] ,
10521146 vec![ "bar-a" , "bar-b" ] ,
10531147 vec![ "bar-b" ] ,
10541148 vec![ "foo-a" ] ,
10551149 ] ;
1150+ let have = package. feature_combinations ( & config) ;
10561151
1057- let actual_combinations = package. feature_combinations ( & config) ;
1058-
1059- sim_assert_eq ! ( expected_combinations, actual_combinations) ;
1152+ sim_assert_eq ! ( have: have, want: want) ;
10601153 Ok ( ( ) )
10611154 }
10621155
@@ -1073,17 +1166,16 @@ mod test {
10731166 exclude_features : HashSet :: from ( [ "bar-a" . to_string ( ) ] ) ,
10741167 ..Default :: default ( )
10751168 } ;
1076- let expected_combinations = vec ! [
1169+ let want = vec ! [
10771170 vec![ ] ,
10781171 vec![ "bar-b" ] ,
10791172 vec![ "foo-a" ] ,
10801173 vec![ "foo-a" , "foo-b" ] ,
10811174 vec![ "foo-b" ] ,
10821175 ] ;
1176+ let have = package. feature_combinations ( & config) ;
10831177
1084- let actual_combinations = package. feature_combinations ( & config) ;
1085-
1086- sim_assert_eq ! ( expected_combinations, actual_combinations) ;
1178+ sim_assert_eq ! ( have: have, want: want) ;
10871179 Ok ( ( ) )
10881180 }
10891181
@@ -1100,11 +1192,10 @@ mod test {
11001192 exclude_features : HashSet :: from ( [ "bar-a" . to_string ( ) ] ) ,
11011193 ..Default :: default ( )
11021194 } ;
1103- let expected_combinations = vec ! [ vec![ ] , vec![ "bar-b" ] , vec![ "foo-a" ] ] ;
1104-
1105- let actual_combinations = package. feature_combinations ( & config) ;
1195+ let want = vec ! [ vec![ ] , vec![ "bar-b" ] , vec![ "foo-a" ] ] ;
1196+ let have = package. feature_combinations ( & config) ;
11061197
1107- sim_assert_eq ! ( expected_combinations , actual_combinations ) ;
1198+ sim_assert_eq ! ( have : have , want : want ) ;
11081199 Ok ( ( ) )
11091200 }
11101201
@@ -1126,12 +1217,10 @@ mod test {
11261217 ] ) ] ,
11271218 ..Default :: default ( )
11281219 } ;
1129- let expected_combinations =
1130- vec ! [ vec![ ] , vec![ "bar-a" , "car-a" ] , vec![ "bar-b" ] , vec![ "foo-a" ] ] ;
1131-
1132- let actual_combinations = package. feature_combinations ( & config) ;
1220+ let want = vec ! [ vec![ ] , vec![ "bar-a" , "car-a" ] , vec![ "bar-b" ] , vec![ "foo-a" ] ] ;
1221+ let have = package. feature_combinations ( & config) ;
11331222
1134- sim_assert_eq ! ( expected_combinations , actual_combinations ) ;
1223+ sim_assert_eq ! ( have : have , want : want ) ;
11351224 Ok ( ( ) )
11361225 }
11371226
@@ -1145,8 +1234,8 @@ mod test {
11451234 . workspace_members ( vec ! [ package. id. clone( ) ] )
11461235 . build ( ) ?;
11471236
1148- let packages = metadata. packages_for_fc ( ) ?;
1149- sim_assert_eq ! ( packages , vec![ & package] ) ;
1237+ let have = metadata. packages_for_fc ( ) ?;
1238+ sim_assert_eq ! ( have : have , want : vec![ & package] ) ;
11501239 Ok ( ( ) )
11511240 }
11521241
@@ -1165,8 +1254,8 @@ mod test {
11651254 } ) )
11661255 . build ( ) ?;
11671256
1168- let packages = metadata. packages_for_fc ( ) ?;
1169- assert ! ( packages . is_empty( ) , "expected no packages after exclusion" ) ;
1257+ let have = metadata. packages_for_fc ( ) ?;
1258+ assert ! ( have . is_empty( ) , "expected no packages after exclusion" ) ;
11701259 Ok ( ( ) )
11711260 }
11721261
0 commit comments