1use crate::DEFAULT_PKG_METADATA_SECTION;
4use crate::cli::{CargoSubcommand, Options, cargo_subcommand};
5use crate::package::{FeatureCombinationError, Package};
6use crate::print_warning;
7use crate::target::TargetTriple;
8
9use color_eyre::eyre;
10use itertools::Itertools;
11use regex::Regex;
12use std::collections::HashSet;
13use std::io::{self, Write};
14use std::process;
15use std::sync::LazyLock;
16use std::time::{Duration, Instant};
17use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
18
19static CYAN: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Cyan, true));
20static RED: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Red, true));
21static YELLOW: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Yellow, true));
22static GREEN: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Green, true));
23static DIMMED: LazyLock<ColorSpec> = LazyLock::new(|| {
24 let mut spec = ColorSpec::new();
25 spec.set_dimmed(true);
26 spec
27});
28
29pub type ExitCode = Option<i32>;
34
35#[must_use]
37pub fn color_spec(color: Color, bold: bool) -> ColorSpec {
38 let mut spec = ColorSpec::new();
39 spec.set_fg(Some(color));
40 spec.set_bold(bold);
41 spec
42}
43
44fn force_color(cmd: &mut process::Command) {
59 cmd.env("CARGO_TERM_COLOR", "always");
60 cmd.env("FORCE_COLOR", "1");
61}
62
63#[derive(Debug, Clone)]
65pub struct Summary {
66 package_name: String,
67 features: Vec<String>,
68 exit_code: Option<i32>,
69 pedantic_success: bool,
70 num_warnings: usize,
71 num_errors: usize,
72 num_suppressed: usize,
73 equivalent_to: Option<Vec<String>>,
75}
76
77impl Summary {
78 fn is_pruned(&self) -> bool {
79 self.equivalent_to.is_some()
80 }
81}
82
83pub fn warning_counts(output: &str) -> impl Iterator<Item = usize> + '_ {
88 static WARNING_REGEX: LazyLock<Regex> = LazyLock::new(|| {
89 #[allow(
90 clippy::expect_used,
91 reason = "hard-coded regex pattern is expected to be valid"
92 )]
93 Regex::new(r"warning: .* generated (\d+) warnings?").expect("valid warning regex")
94 });
95 WARNING_REGEX
96 .captures_iter(output)
97 .filter_map(|cap| cap.get(1))
98 .map(|m| m.as_str().parse::<usize>().unwrap_or(0))
99}
100
101pub fn error_counts(output: &str) -> impl Iterator<Item = usize> + '_ {
106 static ERROR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
107 #[allow(
108 clippy::expect_used,
109 reason = "hard-coded regex pattern is expected to be valid"
110 )]
111 Regex::new(r"error: could not compile `[^`]*`.*due to\s*(\d*)\s*previous errors?")
112 .expect("valid error regex")
113 });
114 ERROR_REGEX
115 .captures_iter(output)
116 .filter_map(|cap| cap.get(1))
117 .map(|m| m.as_str().parse::<usize>().unwrap_or(1))
118}
119
120pub(crate) struct ProcessResult {
122 pub num_warnings: usize,
123 pub num_errors: usize,
124 pub num_suppressed: usize,
125 pub output: Vec<u8>,
126}
127
128fn capture_stderr(
133 child: &mut process::Child,
134 summary_only: bool,
135 stdout: &mut StandardStream,
136) -> io::Result<ProcessResult> {
137 let output_buffer = Vec::<u8>::new();
138 let mut output_cursor = io::Cursor::new(output_buffer);
139
140 if let Some(proc_stderr) = child.stderr.take() {
141 let mut proc_reader = io::BufReader::new(proc_stderr);
142 if summary_only {
143 io::copy(&mut proc_reader, &mut output_cursor)?;
144 } else {
145 let mut tee_reader = crate::tee::Reader::new(proc_reader, stdout, true);
146 io::copy(&mut tee_reader, &mut output_cursor)?;
147 }
148 } else {
149 eprintln!("ERROR: failed to redirect stderr");
150 }
151
152 let stripped = strip_ansi_escapes::strip(output_cursor.get_ref());
153 let stripped = String::from_utf8_lossy(&stripped);
154 let num_warnings = warning_counts(&stripped).sum::<usize>();
155 let num_errors = error_counts(&stripped).sum::<usize>();
156
157 Ok(ProcessResult {
158 num_warnings,
159 num_errors,
160 num_suppressed: 0,
161 output: output_cursor.into_inner(),
162 })
163}
164
165pub(crate) fn print_feature_combination_error(err: &FeatureCombinationError) {
166 let mut stderr = StandardStream::stderr(ColorChoice::Auto);
167
168 let _ = stderr.set_color(&RED);
169 let _ = write!(&mut stderr, "error");
170 let _ = stderr.reset();
171 let _ = writeln!(&mut stderr, ": feature matrix generation failed");
172
173 match err {
174 FeatureCombinationError::TooManyConfigurations {
175 package,
176 num_features,
177 num_configurations,
178 limit,
179 } => {
180 let _ = stderr.set_color(&YELLOW);
181 let _ = writeln!(&mut stderr, " reason: too many configurations");
182 let _ = stderr.reset();
183
184 let _ = stderr.set_color(&CYAN);
185 let _ = write!(&mut stderr, " package:");
186 let _ = stderr.reset();
187 let _ = writeln!(&mut stderr, " {package}");
188
189 let _ = stderr.set_color(&CYAN);
190 let _ = write!(&mut stderr, " features considered:");
191 let _ = stderr.reset();
192 let _ = writeln!(&mut stderr, " {num_features}");
193
194 let _ = stderr.set_color(&CYAN);
195 let _ = write!(&mut stderr, " combinations:");
196 let _ = stderr.reset();
197 let _ = writeln!(
198 &mut stderr,
199 " {}",
200 num_configurations
201 .map(|v| v.to_string())
202 .unwrap_or_else(|| "unbounded".to_string())
203 );
204
205 let _ = stderr.set_color(&CYAN);
206 let _ = write!(&mut stderr, " limit:");
207 let _ = stderr.reset();
208 let _ = writeln!(&mut stderr, " {limit}");
209
210 let _ = stderr.set_color(&GREEN);
211 let _ = writeln!(&mut stderr, " hint:");
212 let _ = stderr.reset();
213 let _ = writeln!(
214 &mut stderr,
215 " Consider restricting the matrix using {DEFAULT_PKG_METADATA_SECTION}.only_features",
216 );
217 let _ = writeln!(
218 &mut stderr,
219 " or splitting features into isolated_feature_sets, or excluding features via exclude_features."
220 );
221 }
222 }
223}
224
225#[must_use]
233pub fn print_summary(
234 summary: &[Summary],
235 show_pruned: bool,
236 mut stdout: termcolor::StandardStream,
237 elapsed: Duration,
238) -> ExitCode {
239 let num_packages = summary
240 .iter()
241 .map(|s| &s.package_name)
242 .collect::<HashSet<_>>()
243 .len();
244 let num_total = summary
245 .iter()
246 .map(|s| (&s.package_name, s.features.iter().collect::<Vec<_>>()))
247 .collect::<HashSet<_>>()
248 .len();
249 let num_pruned = summary.iter().filter(|s| s.is_pruned()).count();
250 let num_executed = num_total - num_pruned;
251
252 println!();
253 stdout.set_color(&CYAN).ok();
254 print!(" Finished ");
255 stdout.reset().ok();
256 if num_pruned > 0 {
257 print!(
258 "{num_executed} of {num_total} feature combination{} for {num_packages} package{} in {:.2}s",
259 if num_total > 1 { "s" } else { "" },
260 if num_packages > 1 { "s" } else { "" },
261 elapsed.as_secs_f64(),
262 );
263 stdout.set_color(&DIMMED).ok();
264 print!(" ({num_pruned} pruned)");
265 stdout.reset().ok();
266 } else {
267 print!(
268 "{num_total} feature combination{} for {num_packages} package{} in {:.2}s",
269 if num_total > 1 { "s" } else { "" },
270 if num_packages > 1 { "s" } else { "" },
271 elapsed.as_secs_f64(),
272 );
273 }
274 println!();
275 println!();
276
277 let max_errors = summary.iter().map(|s| s.num_errors).max().unwrap_or(0);
278 let max_warnings = summary.iter().map(|s| s.num_warnings).max().unwrap_or(0);
279 let max_suppressed = summary.iter().map(|s| s.num_suppressed).max().unwrap_or(0);
280 let show_suppressed = max_suppressed > 0;
281 let errors_width = max_errors.to_string().len();
282 let warnings_width = max_warnings.to_string().len();
283 let suppressed_width = max_suppressed.to_string().len();
284
285 let mut first_bad_exit_code: Option<i32> = None;
286
287 for s in summary {
288 if !show_pruned && s.is_pruned() {
289 continue;
290 }
291 let fmt = SummaryFormat {
292 show_suppressed,
293 errors_width,
294 warnings_width,
295 suppressed_width,
296 };
297 print_summary_entry(s, &mut stdout, &fmt);
298 if !s.pedantic_success {
299 first_bad_exit_code = first_bad_exit_code.or(s.exit_code);
300 }
301 }
302 println!();
303
304 first_bad_exit_code
305}
306
307struct SummaryFormat {
309 show_suppressed: bool,
310 errors_width: usize,
311 warnings_width: usize,
312 suppressed_width: usize,
313}
314
315fn print_summary_entry(s: &Summary, stdout: &mut termcolor::StandardStream, fmt: &SummaryFormat) {
316 if s.is_pruned() {
317 stdout.set_color(&DIMMED).ok();
318 print!(" SKIP ");
319 stdout.reset().ok();
320 } else if !s.pedantic_success {
321 stdout.set_color(&RED).ok();
322 print!(" FAIL ");
323 } else if s.num_warnings > 0 {
324 stdout.set_color(&YELLOW).ok();
325 print!(" WARN ");
326 } else {
327 stdout.set_color(&GREEN).ok();
328 print!(" PASS ");
329 }
330 stdout.reset().ok();
331
332 let feat = s.features.iter().join(", ");
333 let ew = fmt.errors_width;
334 let ww = fmt.warnings_width;
335 let sw = fmt.suppressed_width;
336 let ne = s.num_errors;
337 let nw = s.num_warnings;
338 let ns = s.num_suppressed;
339 if fmt.show_suppressed {
340 print!(
341 "{} ( {ne:>ew$} errors, {nw:>ww$} warnings, {ns:>sw$} suppressed, features = [{feat}] )",
342 s.package_name,
343 );
344 } else {
345 print!(
346 "{} ( {ne:>ew$} errors, {nw:>ww$} warnings, features = [{feat}] )",
347 s.package_name,
348 );
349 }
350
351 if let Some(equiv) = &s.equivalent_to {
352 let equiv = equiv.iter().join(", ");
353 stdout.set_color(&DIMMED).ok();
354 println!(" \u{2190} equivalent to [{equiv}]");
355 stdout.reset().ok();
356 } else {
357 println!();
358 }
359}
360
361fn print_package_cmd(
362 package: &cargo_metadata::Package,
363 features: &[&String],
364 cargo_args: &[&str],
365 all_args: &[&str],
366 options: &Options,
367 stdout: &mut StandardStream,
368) {
369 let compact = options.summary_only || options.diagnostics_only;
370 if !compact {
371 println!();
372 }
373 let subcommand = cargo_subcommand(cargo_args);
374 stdout.set_color(&CYAN).ok();
375 match subcommand {
376 CargoSubcommand::Test => {
377 print!(" Testing ");
378 }
379 CargoSubcommand::Doc => {
380 print!(" Documenting ");
381 }
382 CargoSubcommand::Check => {
383 print!(" Checking ");
384 }
385 CargoSubcommand::Run => {
386 print!(" Running ");
387 }
388 CargoSubcommand::Build => {
389 print!(" Building ");
390 }
391 CargoSubcommand::Other => {
392 print!(" ");
393 }
394 }
395 if subcommand != CargoSubcommand::Other {
399 stdout.reset().ok();
400 }
401 print!(
402 "{} ( features = [{}] )",
403 package.name,
404 features.as_ref().iter().join(", ")
405 );
406 if options.verbose {
407 print!(" [cargo {}]", all_args.join(" "));
408 }
409 stdout.reset().ok();
410 println!();
411 if !compact {
412 println!();
413 }
414}
415
416#[derive(Debug, Default)]
418pub struct MatrixOptions {
419 pub pretty: bool,
421 pub packages_only: bool,
424 pub no_prune_implied: bool,
426}
427
428pub fn print_feature_matrix_for_target(
439 packages: &[&cargo_metadata::Package],
440 target: &TargetTriple,
441 evaluator: &mut impl crate::cfg_eval::CfgEvaluator,
442 options: &MatrixOptions,
443) -> eyre::Result<ExitCode> {
444 let per_package_features = packages
445 .iter()
446 .map(|pkg| {
447 let base_config = pkg.config()?;
448 let config = crate::config::resolve::resolve_config(&base_config, target, evaluator)?;
449 let features = if options.packages_only {
450 vec!["default".to_string()]
451 } else {
452 let combos = pkg.feature_combinations(&config)?;
453 let combos = crate::implication::maybe_prune(
454 combos,
455 &pkg.features,
456 &config,
457 options.no_prune_implied,
458 );
459 combos
460 .keep
461 .into_iter()
462 .map(|combo| combo.into_iter().join(","))
463 .collect()
464 };
465 Ok::<_, eyre::Report>((pkg.name.clone(), config, features))
466 })
467 .collect::<Result<Vec<_>, _>>()?;
468
469 let matrix: Vec<serde_json::Value> = per_package_features
470 .into_iter()
471 .flat_map(|(name, config, features)| {
472 features.into_iter().map(move |ft| {
473 use serde_json_merge::{iter::dfs::Dfs, merge::Merge};
474
475 let mut out = serde_json::json!(config.matrix);
476 out.merge::<Dfs>(&serde_json::json!({
477 "name": name,
478 "features": ft,
479 }));
480 out
481 })
482 })
483 .collect();
484
485 let matrix = if options.pretty {
486 serde_json::to_string_pretty(&matrix)
487 } else {
488 serde_json::to_string(&matrix)
489 }?;
490 println!("{matrix}");
491 Ok(None)
492}
493
494struct RunContext<'a> {
497 cargo_args: &'a [&'a str],
498 extra_args: &'a [&'a str],
499 missing_arguments: bool,
500 use_diagnostics_only: bool,
501 options: &'a Options,
502}
503
504struct CombinationResult {
506 summary: Summary,
507 colored_output: Vec<u8>,
509}
510
511fn run_single_combination(
514 package: &cargo_metadata::Package,
515 features: &[&String],
516 ctx: &RunContext<'_>,
517 seen_diagnostics: &mut HashSet<String>,
518 stdout: &mut StandardStream,
519) -> eyre::Result<CombinationResult> {
520 let Some(working_dir) = package.manifest_path.parent() else {
523 eyre::bail!(
524 "could not find parent dir of package {}",
525 package.manifest_path.to_string()
526 )
527 };
528
529 let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
530 let mut cmd = process::Command::new(&cargo);
531 force_color(&mut cmd);
532
533 if ctx.options.errors_only {
534 cmd.env(
535 "RUSTFLAGS",
536 format!(
537 "-Awarnings {}", std::env::var("RUSTFLAGS").unwrap_or_default()
539 ),
540 );
541 }
542
543 let mut args = ctx.cargo_args.to_vec();
544 if ctx.use_diagnostics_only {
545 args.push(crate::diagnostics_only::MESSAGE_FORMAT);
546 }
547 let features_flag = format!("--features={}", &features.iter().join(","));
548 if !ctx.missing_arguments {
549 args.push("--no-default-features");
550 args.push(&features_flag);
551 }
552 args.extend_from_slice(ctx.extra_args);
553 print_package_cmd(
554 package,
555 features,
556 ctx.cargo_args,
557 &args,
558 ctx.options,
559 stdout,
560 );
561
562 cmd.args(&args).current_dir(working_dir);
563 if ctx.use_diagnostics_only {
564 cmd.stdout(process::Stdio::piped());
565 }
566 cmd.stderr(process::Stdio::piped());
567 let mut child = cmd.spawn()?;
568
569 let mut result = if ctx.use_diagnostics_only {
570 crate::diagnostics_only::process_output(
571 &mut child,
572 ctx.options.summary_only,
573 ctx.options.dedupe,
574 seen_diagnostics,
575 stdout,
576 )?
577 } else {
578 capture_stderr(&mut child, ctx.options.summary_only, stdout)?
579 };
580
581 let exit_status = child.wait()?;
582
583 if result.num_suppressed > 0 && !ctx.options.summary_only {
585 stdout.set_color(&CYAN).ok();
586 print!(" Note ");
587 stdout.reset().ok();
588 println!(
589 "{} duplicate diagnostic{} suppressed",
590 result.num_suppressed,
591 if result.num_suppressed > 1 { "s" } else { "" },
592 );
593 }
594
595 let fail = !exit_status.success();
596
597 if ctx.use_diagnostics_only && fail && result.num_errors == 0 && !result.output.is_empty() {
604 stdout.write_all(&result.output)?;
605 stdout.flush().ok();
606 result.output.clear();
609 }
610
611 let pedantic_fail = ctx.options.pedantic && (result.num_errors > 0 || result.num_warnings > 0);
612
613 let summary = Summary {
614 features: features.iter().map(|s| (*s).clone()).collect(),
615 num_errors: result.num_errors,
616 num_warnings: result.num_warnings,
617 num_suppressed: result.num_suppressed,
618 package_name: package.name.to_string(),
619 exit_code: exit_status.code(),
620 pedantic_success: !(fail || pedantic_fail),
621 equivalent_to: None,
622 };
623
624 Ok(CombinationResult {
625 summary,
626 colored_output: result.output,
627 })
628}
629
630pub fn run_cargo_command_for_target(
640 packages: &[&cargo_metadata::Package],
641 mut cargo_args: Vec<&str>,
642 options: &Options,
643 target: &TargetTriple,
644 evaluator: &mut impl crate::cfg_eval::CfgEvaluator,
645) -> eyre::Result<ExitCode> {
646 let start = Instant::now();
647
648 let extra_args_idx = cargo_args
650 .iter()
651 .position(|arg| *arg == "--")
652 .unwrap_or(cargo_args.len());
653 let extra_args = cargo_args.split_off(extra_args_idx);
654
655 let missing_arguments = cargo_args.is_empty() && extra_args.is_empty();
656
657 if !cargo_args.contains(&"--color") {
658 cargo_args.extend(["--color", "always"]);
660 }
661
662 let user_has_message_format = cargo_args.iter().any(|a| a.starts_with("--message-format"));
664 let use_diagnostics_only = options.diagnostics_only && !user_has_message_format;
665
666 if options.diagnostics_only && user_has_message_format {
667 print_warning!("--diagnostics-only is ignored when --message-format is already specified");
668 }
669
670 let ctx = RunContext {
671 cargo_args: &cargo_args,
672 extra_args: &extra_args,
673 missing_arguments,
674 use_diagnostics_only,
675 options,
676 };
677
678 let mut stdout = StandardStream::stdout(ColorChoice::Auto);
679 let mut summary: Vec<Summary> = Vec::new();
680 let mut seen_diagnostics: HashSet<String> = HashSet::new();
681
682 let mut show_pruned = options.show_pruned;
685
686 for package in packages {
687 let base_config = package.config()?;
688 let config = crate::config::resolve::resolve_config(&base_config, target, evaluator)?;
689 show_pruned = show_pruned || config.show_pruned;
690
691 let all_combos = package.feature_combinations(&config)?;
692 let prune_result = crate::implication::maybe_prune(
693 all_combos,
694 &package.features,
695 &config,
696 options.no_prune_implied,
697 );
698
699 let pkg_start = summary.len();
700 for features in &prune_result.keep {
701 let result = run_single_combination(
702 package,
703 features,
704 &ctx,
705 &mut seen_diagnostics,
706 &mut stdout,
707 )?;
708 let should_stop = options.fail_fast && !result.summary.pedantic_success;
709 let exit_code = result.summary.exit_code;
710 summary.push(result.summary);
711 if should_stop {
712 if options.summary_only {
713 io::copy(&mut io::Cursor::new(result.colored_output), &mut stdout)?;
714 stdout.flush().ok();
715 }
716 let code = print_summary(&summary, show_pruned, stdout, start.elapsed())
717 .or(exit_code)
718 .unwrap_or(1);
719 return Ok(Some(code));
720 }
721 }
722
723 append_pruned_summaries(
724 &mut summary,
725 pkg_start,
726 package.name.as_ref(),
727 prune_result.pruned,
728 );
729 }
730
731 Ok(print_summary(
732 &summary,
733 show_pruned,
734 stdout,
735 start.elapsed(),
736 ))
737}
738
739fn append_pruned_summaries(
743 summary: &mut Vec<Summary>,
744 pkg_start: usize,
745 package_name: &str,
746 pruned: Vec<crate::implication::PrunedCombination>,
747) {
748 let executed: std::collections::HashMap<Vec<String>, Summary> = summary
749 .get(pkg_start..)
750 .unwrap_or_default()
751 .iter()
752 .filter(|s| !s.is_pruned())
753 .map(|s| (s.features.clone(), s.clone()))
754 .collect();
755
756 for p in pruned {
757 let Some(equiv) = executed.get(&p.equivalent_to) else {
758 continue;
759 };
760 summary.push(Summary {
761 package_name: package_name.to_string(),
762 features: p.features,
763 equivalent_to: Some(p.equivalent_to),
764 num_errors: equiv.num_errors,
765 num_warnings: equiv.num_warnings,
766 num_suppressed: equiv.num_suppressed,
767 exit_code: None,
768 pedantic_success: true,
769 });
770 }
771
772 if let Some(slice) = summary.get_mut(pkg_start..) {
773 slice.sort_by(|a, b| a.features.cmp(&b.features));
774 }
775}
776
777#[cfg(test)]
778mod test {
779 use super::{error_counts, warning_counts};
780 use similar_asserts::assert_eq as sim_assert_eq;
781
782 #[test]
783 fn error_regex_single_mod_multiple_errors() {
784 let stderr = include_str!("../test-data/single_mod_multiple_errors_stderr.txt");
785 let errors: Vec<_> = error_counts(stderr).collect();
786 sim_assert_eq!(&errors, &vec![2]);
787 }
788
789 #[test]
790 fn error_regex_with_target_kind() {
791 let stderr =
792 "error: could not compile `docparser-paddleocr-vl` (lib) due to 24 previous errors";
793 let errors: Vec<_> = error_counts(stderr).collect();
794 sim_assert_eq!(&errors, &vec![24]);
795 }
796
797 #[test]
798 fn error_regex_with_target_kind_bin() {
799 let stderr =
800 "error: could not compile `my-crate` (bin \"my-crate\") due to 3 previous errors";
801 let errors: Vec<_> = error_counts(stderr).collect();
802 sim_assert_eq!(&errors, &vec![3]);
803 }
804
805 #[test]
806 fn warning_regex_two_mod_multiple_warnings() {
807 let stderr = include_str!("../test-data/two_mods_warnings_stderr.txt");
808 let warnings: Vec<_> = warning_counts(stderr).collect();
809 sim_assert_eq!(&warnings, &vec![6, 7]);
810 }
811}