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