1use std::backtrace::{Backtrace, BacktraceStatus};
11use std::collections::HashMap;
12use std::ffi::{OsStr, OsString};
13use std::fmt::{Debug, Formatter};
14use std::fs::File;
15use std::hash::Hash;
16use std::io::{BufWriter, Write};
17use std::panic::Location;
18use std::path::Path;
19use std::process::{
20 Child, ChildStderr, ChildStdout, Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio,
21};
22use std::sync::{Arc, Mutex};
23use std::time::{Duration, Instant};
24
25use build_helper::drop_bomb::DropBomb;
26use build_helper::exit;
27
28use crate::core::config::DryRun;
29use crate::{PathBuf, t};
30
31#[derive(Debug, Copy, Clone)]
33pub enum BehaviorOnFailure {
34 Exit,
36 DelayFail,
38 Ignore,
40}
41
42#[derive(Debug, Copy, Clone)]
45pub enum OutputMode {
46 Print,
48 Capture,
50}
51
52impl OutputMode {
53 pub fn captures(&self) -> bool {
54 match self {
55 OutputMode::Print => false,
56 OutputMode::Capture => true,
57 }
58 }
59
60 pub fn stdio(&self) -> Stdio {
61 match self {
62 OutputMode::Print => Stdio::inherit(),
63 OutputMode::Capture => Stdio::piped(),
64 }
65 }
66}
67
68#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
69pub struct CommandFingerprint {
70 program: OsString,
71 args: Vec<OsString>,
72 envs: Vec<(OsString, Option<OsString>)>,
73 cwd: Option<PathBuf>,
74}
75
76impl CommandFingerprint {
77 #[cfg(feature = "tracing")]
78 pub(crate) fn program_name(&self) -> String {
79 Path::new(&self.program)
80 .file_name()
81 .map(|p| p.to_string_lossy().to_string())
82 .unwrap_or_else(|| "<unknown command>".to_string())
83 }
84
85 pub(crate) fn format_short_cmd(&self) -> String {
88 use std::fmt::Write;
89
90 let mut cmd = self.program.to_string_lossy().to_string();
91 for arg in &self.args {
92 let arg = arg.to_string_lossy();
93 if arg.contains(' ') {
94 write!(cmd, " '{arg}'").unwrap();
95 } else {
96 write!(cmd, " {arg}").unwrap();
97 }
98 }
99 if let Some(cwd) = &self.cwd {
100 write!(cmd, " [workdir={}]", cwd.to_string_lossy()).unwrap();
101 }
102 cmd
103 }
104}
105
106#[derive(Default, Clone)]
107pub struct CommandProfile {
108 pub traces: Vec<ExecutionTrace>,
109}
110
111#[derive(Default)]
112pub struct CommandProfiler {
113 stats: Mutex<HashMap<CommandFingerprint, CommandProfile>>,
114}
115
116impl CommandProfiler {
117 pub fn record_execution(&self, key: CommandFingerprint, start_time: Instant) {
118 let mut stats = self.stats.lock().unwrap();
119 let entry = stats.entry(key).or_default();
120 entry.traces.push(ExecutionTrace::Executed { duration: start_time.elapsed() });
121 }
122
123 pub fn record_cache_hit(&self, key: CommandFingerprint) {
124 let mut stats = self.stats.lock().unwrap();
125 let entry = stats.entry(key).or_default();
126 entry.traces.push(ExecutionTrace::CacheHit);
127 }
128
129 pub fn report_summary(&self, path: &Path, start_time: Instant) {
131 let file = t!(File::create(path));
132
133 let mut writer = BufWriter::new(file);
134 let stats = self.stats.lock().unwrap();
135
136 let mut entries: Vec<_> = stats
137 .iter()
138 .map(|(key, profile)| {
139 let max_duration = profile
140 .traces
141 .iter()
142 .filter_map(|trace| match trace {
143 ExecutionTrace::Executed { duration, .. } => Some(*duration),
144 _ => None,
145 })
146 .max();
147
148 (key, profile, max_duration)
149 })
150 .collect();
151
152 entries.sort_by_key(|e| std::cmp::Reverse(e.2));
153
154 let total_bootstrap_duration = start_time.elapsed();
155
156 let total_fingerprints = entries.len();
157 let mut total_cache_hits = 0;
158 let mut total_execution_duration = Duration::ZERO;
159 let mut total_saved_duration = Duration::ZERO;
160
161 for (key, profile, max_duration) in &entries {
162 writeln!(writer, "Command: {:?}", key.format_short_cmd()).unwrap();
163
164 let mut hits = 0;
165 let mut runs = 0;
166 let mut command_total_duration = Duration::ZERO;
167
168 for trace in &profile.traces {
169 match trace {
170 ExecutionTrace::CacheHit => {
171 hits += 1;
172 }
173 ExecutionTrace::Executed { duration, .. } => {
174 runs += 1;
175 command_total_duration += *duration;
176 }
177 }
178 }
179
180 total_cache_hits += hits;
181 total_execution_duration += command_total_duration;
182 total_saved_duration += command_total_duration * hits as u32;
188
189 let command_vs_bootstrap = if total_bootstrap_duration > Duration::ZERO {
190 100.0 * command_total_duration.as_secs_f64()
191 / total_bootstrap_duration.as_secs_f64()
192 } else {
193 0.0
194 };
195
196 let duration_str = match max_duration {
197 Some(d) => format!("{d:.2?}"),
198 None => "-".into(),
199 };
200
201 writeln!(
202 writer,
203 "Summary: {runs} run(s), {hits} hit(s), max_duration={duration_str} total_duration: {command_total_duration:.2?} ({command_vs_bootstrap:.2?}% of total)\n"
204 )
205 .unwrap();
206 }
207
208 let overhead_time = total_bootstrap_duration
209 .checked_sub(total_execution_duration)
210 .unwrap_or(Duration::ZERO);
211
212 writeln!(writer, "\n=== Aggregated Summary ===").unwrap();
213 writeln!(writer, "Total unique commands (fingerprints): {total_fingerprints}").unwrap();
214 writeln!(writer, "Total time spent in command executions: {total_execution_duration:.2?}")
215 .unwrap();
216 writeln!(writer, "Total bootstrap time: {total_bootstrap_duration:.2?}").unwrap();
217 writeln!(writer, "Time spent outside command executions: {overhead_time:.2?}").unwrap();
218 writeln!(writer, "Total cache hits: {total_cache_hits}").unwrap();
219 writeln!(writer, "Estimated time saved due to cache hits: {total_saved_duration:.2?}")
220 .unwrap();
221 }
222}
223
224#[derive(Clone)]
225pub enum ExecutionTrace {
226 CacheHit,
227 Executed { duration: Duration },
228}
229
230pub struct BootstrapCommand {
247 command: Command,
248 pub failure_behavior: BehaviorOnFailure,
249 pub run_in_dry_run: bool,
251 drop_bomb: DropBomb,
254 should_cache: bool,
255}
256
257impl<'a> BootstrapCommand {
258 #[track_caller]
259 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
260 Command::new(program).into()
261 }
262 pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
263 self.command.arg(arg.as_ref());
264 self
265 }
266
267 pub fn cached(&mut self) -> &mut Self {
271 self.should_cache = true;
272 self
273 }
274
275 pub fn args<I, S>(&mut self, args: I) -> &mut Self
276 where
277 I: IntoIterator<Item = S>,
278 S: AsRef<OsStr>,
279 {
280 self.command.args(args);
281 self
282 }
283
284 pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
285 where
286 K: AsRef<OsStr>,
287 V: AsRef<OsStr>,
288 {
289 self.command.env(key, val);
290 self
291 }
292
293 pub fn get_envs(&self) -> CommandEnvs<'_> {
294 self.command.get_envs()
295 }
296
297 pub fn get_args(&self) -> CommandArgs<'_> {
298 self.command.get_args()
299 }
300
301 pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
302 self.command.env_remove(key);
303 self
304 }
305
306 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
307 self.command.current_dir(dir);
308 self
309 }
310
311 pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
312 self.command.stdin(stdin);
313 self
314 }
315
316 #[must_use]
317 pub fn delay_failure(self) -> Self {
318 Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
319 }
320
321 pub fn fail_fast(self) -> Self {
322 Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
323 }
324
325 #[must_use]
326 pub fn allow_failure(self) -> Self {
327 Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
328 }
329
330 pub fn run_in_dry_run(&mut self) -> &mut Self {
331 self.run_in_dry_run = true;
332 self
333 }
334
335 #[track_caller]
338 pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
339 exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
340 }
341
342 #[track_caller]
344 pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
345 exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
346 }
347
348 #[track_caller]
350 pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
351 exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
352 }
353
354 #[track_caller]
356 pub fn start_capture(
357 &'a mut self,
358 exec_ctx: impl AsRef<ExecutionContext>,
359 ) -> DeferredCommand<'a> {
360 exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
361 }
362
363 #[track_caller]
365 pub fn start_capture_stdout(
366 &'a mut self,
367 exec_ctx: impl AsRef<ExecutionContext>,
368 ) -> DeferredCommand<'a> {
369 exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
370 }
371
372 #[track_caller]
375 pub fn stream_capture_stdout(
376 &'a mut self,
377 exec_ctx: impl AsRef<ExecutionContext>,
378 ) -> Option<StreamingCommand> {
379 exec_ctx.as_ref().stream(self, OutputMode::Capture, OutputMode::Print)
380 }
381
382 pub fn mark_as_executed(&mut self) {
385 self.drop_bomb.defuse();
386 }
387
388 pub fn get_created_location(&self) -> std::panic::Location<'static> {
390 self.drop_bomb.get_created_location()
391 }
392
393 pub fn fingerprint(&self) -> CommandFingerprint {
394 let command = &self.command;
395 CommandFingerprint {
396 program: command.get_program().into(),
397 args: command.get_args().map(OsStr::to_os_string).collect(),
398 envs: command
399 .get_envs()
400 .map(|(k, v)| (k.to_os_string(), v.map(|val| val.to_os_string())))
401 .collect(),
402 cwd: command.get_current_dir().map(Path::to_path_buf),
403 }
404 }
405}
406
407impl Debug for BootstrapCommand {
408 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
409 write!(f, "{:?}", self.command)?;
410 write!(f, " (failure_mode={:?})", self.failure_behavior)
411 }
412}
413
414impl From<Command> for BootstrapCommand {
415 #[track_caller]
416 fn from(command: Command) -> Self {
417 let program = command.get_program().to_owned();
418 Self {
419 should_cache: false,
420 command,
421 failure_behavior: BehaviorOnFailure::Exit,
422 run_in_dry_run: false,
423 drop_bomb: DropBomb::arm(program),
424 }
425 }
426}
427
428#[derive(Clone, PartialEq)]
430enum CommandStatus {
431 Finished(ExitStatus),
433 DidNotStartOrFinish,
435}
436
437#[track_caller]
440#[must_use]
441pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
442 BootstrapCommand::new(program)
443}
444
445#[derive(Clone, PartialEq)]
447pub struct CommandOutput {
448 status: CommandStatus,
449 stdout: Option<Vec<u8>>,
450 stderr: Option<Vec<u8>>,
451}
452
453impl CommandOutput {
454 #[must_use]
455 pub fn not_finished(stdout: OutputMode, stderr: OutputMode) -> Self {
456 Self {
457 status: CommandStatus::DidNotStartOrFinish,
458 stdout: match stdout {
459 OutputMode::Print => None,
460 OutputMode::Capture => Some(vec![]),
461 },
462 stderr: match stderr {
463 OutputMode::Print => None,
464 OutputMode::Capture => Some(vec![]),
465 },
466 }
467 }
468
469 #[must_use]
470 pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
471 Self {
472 status: CommandStatus::Finished(output.status),
473 stdout: match stdout {
474 OutputMode::Print => None,
475 OutputMode::Capture => Some(output.stdout),
476 },
477 stderr: match stderr {
478 OutputMode::Print => None,
479 OutputMode::Capture => Some(output.stderr),
480 },
481 }
482 }
483
484 #[must_use]
485 pub fn is_success(&self) -> bool {
486 match self.status {
487 CommandStatus::Finished(status) => status.success(),
488 CommandStatus::DidNotStartOrFinish => false,
489 }
490 }
491
492 #[must_use]
493 pub fn is_failure(&self) -> bool {
494 !self.is_success()
495 }
496
497 pub fn status(&self) -> Option<ExitStatus> {
498 match self.status {
499 CommandStatus::Finished(status) => Some(status),
500 CommandStatus::DidNotStartOrFinish => None,
501 }
502 }
503
504 #[must_use]
505 pub fn stdout(&self) -> String {
506 String::from_utf8(
507 self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
508 )
509 .expect("Cannot parse process stdout as UTF-8")
510 }
511
512 #[must_use]
513 pub fn stdout_if_present(&self) -> Option<String> {
514 self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
515 }
516
517 #[must_use]
518 pub fn stdout_if_ok(&self) -> Option<String> {
519 if self.is_success() { Some(self.stdout()) } else { None }
520 }
521
522 #[must_use]
523 pub fn stderr(&self) -> String {
524 String::from_utf8(
525 self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
526 )
527 .expect("Cannot parse process stderr as UTF-8")
528 }
529
530 #[must_use]
531 pub fn stderr_if_present(&self) -> Option<String> {
532 self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
533 }
534}
535
536impl Default for CommandOutput {
537 fn default() -> Self {
538 Self {
539 status: CommandStatus::Finished(ExitStatus::default()),
540 stdout: Some(vec![]),
541 stderr: Some(vec![]),
542 }
543 }
544}
545
546#[derive(Clone, Default)]
547pub struct ExecutionContext {
548 dry_run: DryRun,
549 pub verbosity: u8,
550 pub fail_fast: bool,
551 delayed_failures: Arc<Mutex<Vec<String>>>,
552 command_cache: Arc<CommandCache>,
553 profiler: Arc<CommandProfiler>,
554}
555
556#[derive(Default)]
557pub struct CommandCache {
558 cache: Mutex<HashMap<CommandFingerprint, CommandOutput>>,
559}
560
561enum CommandState<'a> {
562 Cached(CommandOutput),
563 Deferred {
564 process: Option<Result<Child, std::io::Error>>,
565 command: &'a mut BootstrapCommand,
566 stdout: OutputMode,
567 stderr: OutputMode,
568 executed_at: &'a Location<'a>,
569 fingerprint: CommandFingerprint,
570 start_time: Instant,
571 #[cfg(feature = "tracing")]
572 _span_guard: tracing::span::EnteredSpan,
573 },
574}
575
576pub struct StreamingCommand {
577 child: Child,
578 pub stdout: Option<ChildStdout>,
579 pub stderr: Option<ChildStderr>,
580 fingerprint: CommandFingerprint,
581 start_time: Instant,
582 #[cfg(feature = "tracing")]
583 _span_guard: tracing::span::EnteredSpan,
584}
585
586#[must_use]
587pub struct DeferredCommand<'a> {
588 state: CommandState<'a>,
589}
590
591impl CommandCache {
592 pub fn get(&self, key: &CommandFingerprint) -> Option<CommandOutput> {
593 self.cache.lock().unwrap().get(key).cloned()
594 }
595
596 pub fn insert(&self, key: CommandFingerprint, output: CommandOutput) {
597 self.cache.lock().unwrap().insert(key, output);
598 }
599}
600
601impl ExecutionContext {
602 pub fn new(verbosity: u8, fail_fast: bool) -> Self {
603 Self { verbosity, fail_fast, ..Default::default() }
604 }
605
606 pub fn dry_run(&self) -> bool {
607 match self.dry_run {
608 DryRun::Disabled => false,
609 DryRun::SelfCheck | DryRun::UserSelected => true,
610 }
611 }
612
613 pub fn profiler(&self) -> &CommandProfiler {
614 &self.profiler
615 }
616
617 pub fn get_dry_run(&self) -> &DryRun {
618 &self.dry_run
619 }
620
621 pub fn do_if_verbose(&self, f: impl Fn()) {
622 if self.is_verbose() {
623 f()
624 }
625 }
626
627 pub fn is_verbose(&self) -> bool {
628 self.verbosity > 0
629 }
630
631 pub fn fail_fast(&self) -> bool {
632 self.fail_fast
633 }
634
635 pub fn set_dry_run(&mut self, value: DryRun) {
636 self.dry_run = value;
637 }
638
639 pub fn set_verbosity(&mut self, value: u8) {
640 self.verbosity = value;
641 }
642
643 pub fn set_fail_fast(&mut self, value: bool) {
644 self.fail_fast = value;
645 }
646
647 pub fn add_to_delay_failure(&self, message: String) {
648 self.delayed_failures.lock().unwrap().push(message);
649 }
650
651 pub fn report_failures_and_exit(&self) {
652 let failures = self.delayed_failures.lock().unwrap();
653 if failures.is_empty() {
654 return;
655 }
656 eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
657 for failure in &*failures {
658 eprintln!(" - {failure}");
659 }
660 exit!(1);
661 }
662
663 #[track_caller]
667 pub fn start<'a>(
668 &self,
669 command: &'a mut BootstrapCommand,
670 stdout: OutputMode,
671 stderr: OutputMode,
672 ) -> DeferredCommand<'a> {
673 let fingerprint = command.fingerprint();
674
675 if let Some(cached_output) = self.command_cache.get(&fingerprint) {
676 command.mark_as_executed();
677 self.do_if_verbose(|| println!("Cache hit: {command:?}"));
678 self.profiler.record_cache_hit(fingerprint);
679 return DeferredCommand { state: CommandState::Cached(cached_output) };
680 }
681
682 #[cfg(feature = "tracing")]
683 let span_guard = crate::utils::tracing::trace_cmd(command);
684
685 let created_at = command.get_created_location();
686 let executed_at = std::panic::Location::caller();
687
688 if self.dry_run() && !command.run_in_dry_run {
689 return DeferredCommand {
690 state: CommandState::Deferred {
691 process: None,
692 command,
693 stdout,
694 stderr,
695 executed_at,
696 fingerprint,
697 start_time: Instant::now(),
698 #[cfg(feature = "tracing")]
699 _span_guard: span_guard,
700 },
701 };
702 }
703
704 self.do_if_verbose(|| {
705 println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
706 });
707
708 let cmd = &mut command.command;
709 cmd.stdout(stdout.stdio());
710 cmd.stderr(stderr.stdio());
711
712 let start_time = Instant::now();
713
714 let child = cmd.spawn();
715
716 DeferredCommand {
717 state: CommandState::Deferred {
718 process: Some(child),
719 command,
720 stdout,
721 stderr,
722 executed_at,
723 fingerprint,
724 start_time,
725 #[cfg(feature = "tracing")]
726 _span_guard: span_guard,
727 },
728 }
729 }
730
731 #[track_caller]
735 pub fn run(
736 &self,
737 command: &mut BootstrapCommand,
738 stdout: OutputMode,
739 stderr: OutputMode,
740 ) -> CommandOutput {
741 self.start(command, stdout, stderr).wait_for_output(self)
742 }
743
744 fn fail(&self, message: &str) -> ! {
745 println!("{message}");
746
747 if !self.is_verbose() {
748 println!("Command has failed. Rerun with -v to see more details.");
749 }
750 exit!(1);
751 }
752
753 pub fn stream(
757 &self,
758 command: &mut BootstrapCommand,
759 stdout: OutputMode,
760 stderr: OutputMode,
761 ) -> Option<StreamingCommand> {
762 command.mark_as_executed();
763 if !command.run_in_dry_run && self.dry_run() {
764 return None;
765 }
766
767 #[cfg(feature = "tracing")]
768 let span_guard = crate::utils::tracing::trace_cmd(command);
769
770 let start_time = Instant::now();
771 let fingerprint = command.fingerprint();
772 let cmd = &mut command.command;
773 cmd.stdout(stdout.stdio());
774 cmd.stderr(stderr.stdio());
775 let child = cmd.spawn();
776 let mut child = match child {
777 Ok(child) => child,
778 Err(e) => panic!("failed to execute command: {cmd:?}\nERROR: {e}"),
779 };
780
781 let stdout = child.stdout.take();
782 let stderr = child.stderr.take();
783 Some(StreamingCommand {
784 child,
785 stdout,
786 stderr,
787 fingerprint,
788 start_time,
789 #[cfg(feature = "tracing")]
790 _span_guard: span_guard,
791 })
792 }
793}
794
795impl AsRef<ExecutionContext> for ExecutionContext {
796 fn as_ref(&self) -> &ExecutionContext {
797 self
798 }
799}
800
801impl StreamingCommand {
802 pub fn wait(
803 mut self,
804 exec_ctx: impl AsRef<ExecutionContext>,
805 ) -> Result<ExitStatus, std::io::Error> {
806 let exec_ctx = exec_ctx.as_ref();
807 let output = self.child.wait();
808 exec_ctx.profiler().record_execution(self.fingerprint, self.start_time);
809 output
810 }
811}
812
813impl<'a> DeferredCommand<'a> {
814 pub fn wait_for_output(self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
815 match self.state {
816 CommandState::Cached(output) => output,
817 CommandState::Deferred {
818 process,
819 command,
820 stdout,
821 stderr,
822 executed_at,
823 fingerprint,
824 start_time,
825 #[cfg(feature = "tracing")]
826 _span_guard,
827 } => {
828 let exec_ctx = exec_ctx.as_ref();
829
830 let output =
831 Self::finish_process(process, command, stdout, stderr, executed_at, exec_ctx);
832
833 #[cfg(feature = "tracing")]
834 drop(_span_guard);
835
836 if (!exec_ctx.dry_run() || command.run_in_dry_run)
837 && output.status().is_some()
838 && command.should_cache
839 {
840 exec_ctx.command_cache.insert(fingerprint.clone(), output.clone());
841 exec_ctx.profiler.record_execution(fingerprint, start_time);
842 }
843
844 output
845 }
846 }
847 }
848
849 pub fn finish_process(
850 mut process: Option<Result<Child, std::io::Error>>,
851 command: &mut BootstrapCommand,
852 stdout: OutputMode,
853 stderr: OutputMode,
854 executed_at: &'a std::panic::Location<'a>,
855 exec_ctx: &ExecutionContext,
856 ) -> CommandOutput {
857 use std::fmt::Write;
858
859 command.mark_as_executed();
860
861 let process = match process.take() {
862 Some(p) => p,
863 None => return CommandOutput::default(),
864 };
865
866 let created_at = command.get_created_location();
867
868 #[allow(clippy::enum_variant_names)]
869 enum FailureReason {
870 FailedAtRuntime(ExitStatus),
871 FailedToFinish(std::io::Error),
872 FailedToStart(std::io::Error),
873 }
874
875 let (output, fail_reason) = match process {
876 Ok(child) => match child.wait_with_output() {
877 Ok(output) if output.status.success() => {
878 (CommandOutput::from_output(output, stdout, stderr), None)
880 }
881 Ok(output) => {
882 let status = output.status;
884 (
885 CommandOutput::from_output(output, stdout, stderr),
886 Some(FailureReason::FailedAtRuntime(status)),
887 )
888 }
889 Err(e) => {
890 (
892 CommandOutput::not_finished(stdout, stderr),
893 Some(FailureReason::FailedToFinish(e)),
894 )
895 }
896 },
897 Err(e) => {
898 (CommandOutput::not_finished(stdout, stderr), Some(FailureReason::FailedToStart(e)))
900 }
901 };
902
903 if let Some(fail_reason) = fail_reason {
904 let mut error_message = String::new();
905 let command_str = if exec_ctx.is_verbose() {
906 format!("{command:?}")
907 } else {
908 command.fingerprint().format_short_cmd()
909 };
910 let action = match fail_reason {
911 FailureReason::FailedAtRuntime(e) => {
912 format!("failed with exit code {}", e.code().unwrap_or(1))
913 }
914 FailureReason::FailedToFinish(e) => {
915 format!("failed to finish: {e:?}")
916 }
917 FailureReason::FailedToStart(e) => {
918 format!("failed to start: {e:?}")
919 }
920 };
921 writeln!(
922 error_message,
923 r#"Command `{command_str}` {action}
924Created at: {created_at}
925Executed at: {executed_at}"#,
926 )
927 .unwrap();
928 if stdout.captures() {
929 writeln!(error_message, "\n--- STDOUT vvv\n{}", output.stdout().trim()).unwrap();
930 }
931 if stderr.captures() {
932 writeln!(error_message, "\n--- STDERR vvv\n{}", output.stderr().trim()).unwrap();
933 }
934 let backtrace = if exec_ctx.verbosity > 1 {
935 Backtrace::force_capture()
936 } else if matches!(command.failure_behavior, BehaviorOnFailure::Ignore) {
937 Backtrace::disabled()
938 } else {
939 Backtrace::capture()
940 };
941 if matches!(backtrace.status(), BacktraceStatus::Captured) {
942 writeln!(error_message, "\n--- BACKTRACE vvv\n{backtrace}").unwrap();
943 }
944
945 match command.failure_behavior {
946 BehaviorOnFailure::DelayFail => {
947 if exec_ctx.fail_fast {
948 exec_ctx.fail(&error_message);
949 }
950 exec_ctx.add_to_delay_failure(error_message);
951 }
952 BehaviorOnFailure::Exit => {
953 exec_ctx.fail(&error_message);
954 }
955 BehaviorOnFailure::Ignore => {
956 }
960 }
961 }
962
963 output
964 }
965}