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::ci::CiEnv;
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(|a, b| b.2.cmp(&a.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 do_not_cache(&mut self) -> &mut Self {
268 self.should_cache = false;
269 self
270 }
271
272 pub fn args<I, S>(&mut self, args: I) -> &mut Self
273 where
274 I: IntoIterator<Item = S>,
275 S: AsRef<OsStr>,
276 {
277 self.command.args(args);
278 self
279 }
280
281 pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
282 where
283 K: AsRef<OsStr>,
284 V: AsRef<OsStr>,
285 {
286 self.command.env(key, val);
287 self
288 }
289
290 pub fn get_envs(&self) -> CommandEnvs<'_> {
291 self.command.get_envs()
292 }
293
294 pub fn get_args(&self) -> CommandArgs<'_> {
295 self.command.get_args()
296 }
297
298 pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
299 self.command.env_remove(key);
300 self
301 }
302
303 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
304 self.command.current_dir(dir);
305 self
306 }
307
308 pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
309 self.command.stdin(stdin);
310 self
311 }
312
313 #[must_use]
314 pub fn delay_failure(self) -> Self {
315 Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
316 }
317
318 pub fn fail_fast(self) -> Self {
319 Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
320 }
321
322 #[must_use]
323 pub fn allow_failure(self) -> Self {
324 Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
325 }
326
327 pub fn run_in_dry_run(&mut self) -> &mut Self {
328 self.run_in_dry_run = true;
329 self
330 }
331
332 #[track_caller]
335 pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
336 exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
337 }
338
339 #[track_caller]
341 pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
342 exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
343 }
344
345 #[track_caller]
347 pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
348 exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
349 }
350
351 #[track_caller]
353 pub fn start_capture(
354 &'a mut self,
355 exec_ctx: impl AsRef<ExecutionContext>,
356 ) -> DeferredCommand<'a> {
357 exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
358 }
359
360 #[track_caller]
362 pub fn start_capture_stdout(
363 &'a mut self,
364 exec_ctx: impl AsRef<ExecutionContext>,
365 ) -> DeferredCommand<'a> {
366 exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
367 }
368
369 #[track_caller]
372 pub fn stream_capture_stdout(
373 &'a mut self,
374 exec_ctx: impl AsRef<ExecutionContext>,
375 ) -> Option<StreamingCommand> {
376 exec_ctx.as_ref().stream(self, OutputMode::Capture, OutputMode::Print)
377 }
378
379 pub fn mark_as_executed(&mut self) {
382 self.drop_bomb.defuse();
383 }
384
385 pub fn get_created_location(&self) -> std::panic::Location<'static> {
387 self.drop_bomb.get_created_location()
388 }
389
390 pub fn force_coloring_in_ci(&mut self) {
392 if CiEnv::is_ci() {
393 self.env("TERM", "xterm").args(["--color", "always"]);
399 }
400 }
401
402 pub fn fingerprint(&self) -> CommandFingerprint {
403 let command = &self.command;
404 CommandFingerprint {
405 program: command.get_program().into(),
406 args: command.get_args().map(OsStr::to_os_string).collect(),
407 envs: command
408 .get_envs()
409 .map(|(k, v)| (k.to_os_string(), v.map(|val| val.to_os_string())))
410 .collect(),
411 cwd: command.get_current_dir().map(Path::to_path_buf),
412 }
413 }
414}
415
416impl Debug for BootstrapCommand {
417 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
418 write!(f, "{:?}", self.command)?;
419 write!(f, " (failure_mode={:?})", self.failure_behavior)
420 }
421}
422
423impl From<Command> for BootstrapCommand {
424 #[track_caller]
425 fn from(command: Command) -> Self {
426 let program = command.get_program().to_owned();
427 Self {
428 should_cache: true,
429 command,
430 failure_behavior: BehaviorOnFailure::Exit,
431 run_in_dry_run: false,
432 drop_bomb: DropBomb::arm(program),
433 }
434 }
435}
436
437#[derive(Clone, PartialEq)]
439enum CommandStatus {
440 Finished(ExitStatus),
442 DidNotStartOrFinish,
444}
445
446#[track_caller]
449#[must_use]
450pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
451 BootstrapCommand::new(program)
452}
453
454#[derive(Clone, PartialEq)]
456pub struct CommandOutput {
457 status: CommandStatus,
458 stdout: Option<Vec<u8>>,
459 stderr: Option<Vec<u8>>,
460}
461
462impl CommandOutput {
463 #[must_use]
464 pub fn not_finished(stdout: OutputMode, stderr: OutputMode) -> Self {
465 Self {
466 status: CommandStatus::DidNotStartOrFinish,
467 stdout: match stdout {
468 OutputMode::Print => None,
469 OutputMode::Capture => Some(vec![]),
470 },
471 stderr: match stderr {
472 OutputMode::Print => None,
473 OutputMode::Capture => Some(vec![]),
474 },
475 }
476 }
477
478 #[must_use]
479 pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
480 Self {
481 status: CommandStatus::Finished(output.status),
482 stdout: match stdout {
483 OutputMode::Print => None,
484 OutputMode::Capture => Some(output.stdout),
485 },
486 stderr: match stderr {
487 OutputMode::Print => None,
488 OutputMode::Capture => Some(output.stderr),
489 },
490 }
491 }
492
493 #[must_use]
494 pub fn is_success(&self) -> bool {
495 match self.status {
496 CommandStatus::Finished(status) => status.success(),
497 CommandStatus::DidNotStartOrFinish => false,
498 }
499 }
500
501 #[must_use]
502 pub fn is_failure(&self) -> bool {
503 !self.is_success()
504 }
505
506 pub fn status(&self) -> Option<ExitStatus> {
507 match self.status {
508 CommandStatus::Finished(status) => Some(status),
509 CommandStatus::DidNotStartOrFinish => None,
510 }
511 }
512
513 #[must_use]
514 pub fn stdout(&self) -> String {
515 String::from_utf8(
516 self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
517 )
518 .expect("Cannot parse process stdout as UTF-8")
519 }
520
521 #[must_use]
522 pub fn stdout_if_present(&self) -> Option<String> {
523 self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
524 }
525
526 #[must_use]
527 pub fn stdout_if_ok(&self) -> Option<String> {
528 if self.is_success() { Some(self.stdout()) } else { None }
529 }
530
531 #[must_use]
532 pub fn stderr(&self) -> String {
533 String::from_utf8(
534 self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
535 )
536 .expect("Cannot parse process stderr as UTF-8")
537 }
538
539 #[must_use]
540 pub fn stderr_if_present(&self) -> Option<String> {
541 self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
542 }
543}
544
545impl Default for CommandOutput {
546 fn default() -> Self {
547 Self {
548 status: CommandStatus::Finished(ExitStatus::default()),
549 stdout: Some(vec![]),
550 stderr: Some(vec![]),
551 }
552 }
553}
554
555#[derive(Clone, Default)]
556pub struct ExecutionContext {
557 dry_run: DryRun,
558 pub verbosity: u8,
559 pub fail_fast: bool,
560 delayed_failures: Arc<Mutex<Vec<String>>>,
561 command_cache: Arc<CommandCache>,
562 profiler: Arc<CommandProfiler>,
563}
564
565#[derive(Default)]
566pub struct CommandCache {
567 cache: Mutex<HashMap<CommandFingerprint, CommandOutput>>,
568}
569
570enum CommandState<'a> {
571 Cached(CommandOutput),
572 Deferred {
573 process: Option<Result<Child, std::io::Error>>,
574 command: &'a mut BootstrapCommand,
575 stdout: OutputMode,
576 stderr: OutputMode,
577 executed_at: &'a Location<'a>,
578 fingerprint: CommandFingerprint,
579 start_time: Instant,
580 #[cfg(feature = "tracing")]
581 _span_guard: tracing::span::EnteredSpan,
582 },
583}
584
585pub struct StreamingCommand {
586 child: Child,
587 pub stdout: Option<ChildStdout>,
588 pub stderr: Option<ChildStderr>,
589 fingerprint: CommandFingerprint,
590 start_time: Instant,
591 #[cfg(feature = "tracing")]
592 _span_guard: tracing::span::EnteredSpan,
593}
594
595#[must_use]
596pub struct DeferredCommand<'a> {
597 state: CommandState<'a>,
598}
599
600impl CommandCache {
601 pub fn get(&self, key: &CommandFingerprint) -> Option<CommandOutput> {
602 self.cache.lock().unwrap().get(key).cloned()
603 }
604
605 pub fn insert(&self, key: CommandFingerprint, output: CommandOutput) {
606 self.cache.lock().unwrap().insert(key, output);
607 }
608}
609
610impl ExecutionContext {
611 pub fn new(verbosity: u8, fail_fast: bool) -> Self {
612 Self { verbosity, fail_fast, ..Default::default() }
613 }
614
615 pub fn dry_run(&self) -> bool {
616 match self.dry_run {
617 DryRun::Disabled => false,
618 DryRun::SelfCheck | DryRun::UserSelected => true,
619 }
620 }
621
622 pub fn profiler(&self) -> &CommandProfiler {
623 &self.profiler
624 }
625
626 pub fn get_dry_run(&self) -> &DryRun {
627 &self.dry_run
628 }
629
630 pub fn verbose(&self, f: impl Fn()) {
631 if self.is_verbose() {
632 f()
633 }
634 }
635
636 pub fn is_verbose(&self) -> bool {
637 self.verbosity > 0
638 }
639
640 pub fn fail_fast(&self) -> bool {
641 self.fail_fast
642 }
643
644 pub fn set_dry_run(&mut self, value: DryRun) {
645 self.dry_run = value;
646 }
647
648 pub fn set_verbosity(&mut self, value: u8) {
649 self.verbosity = value;
650 }
651
652 pub fn set_fail_fast(&mut self, value: bool) {
653 self.fail_fast = value;
654 }
655
656 pub fn add_to_delay_failure(&self, message: String) {
657 self.delayed_failures.lock().unwrap().push(message);
658 }
659
660 pub fn report_failures_and_exit(&self) {
661 let failures = self.delayed_failures.lock().unwrap();
662 if failures.is_empty() {
663 return;
664 }
665 eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
666 for failure in &*failures {
667 eprintln!(" - {failure}");
668 }
669 exit!(1);
670 }
671
672 #[track_caller]
676 pub fn start<'a>(
677 &self,
678 command: &'a mut BootstrapCommand,
679 stdout: OutputMode,
680 stderr: OutputMode,
681 ) -> DeferredCommand<'a> {
682 let fingerprint = command.fingerprint();
683
684 if let Some(cached_output) = self.command_cache.get(&fingerprint) {
685 command.mark_as_executed();
686 self.verbose(|| println!("Cache hit: {command:?}"));
687 self.profiler.record_cache_hit(fingerprint);
688 return DeferredCommand { state: CommandState::Cached(cached_output) };
689 }
690
691 #[cfg(feature = "tracing")]
692 let span_guard = crate::utils::tracing::trace_cmd(command);
693
694 let created_at = command.get_created_location();
695 let executed_at = std::panic::Location::caller();
696
697 if self.dry_run() && !command.run_in_dry_run {
698 return DeferredCommand {
699 state: CommandState::Deferred {
700 process: None,
701 command,
702 stdout,
703 stderr,
704 executed_at,
705 fingerprint,
706 start_time: Instant::now(),
707 #[cfg(feature = "tracing")]
708 _span_guard: span_guard,
709 },
710 };
711 }
712
713 self.verbose(|| {
714 println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
715 });
716
717 let cmd = &mut command.command;
718 cmd.stdout(stdout.stdio());
719 cmd.stderr(stderr.stdio());
720
721 let start_time = Instant::now();
722
723 let child = cmd.spawn();
724
725 DeferredCommand {
726 state: CommandState::Deferred {
727 process: Some(child),
728 command,
729 stdout,
730 stderr,
731 executed_at,
732 fingerprint,
733 start_time,
734 #[cfg(feature = "tracing")]
735 _span_guard: span_guard,
736 },
737 }
738 }
739
740 #[track_caller]
744 pub fn run(
745 &self,
746 command: &mut BootstrapCommand,
747 stdout: OutputMode,
748 stderr: OutputMode,
749 ) -> CommandOutput {
750 self.start(command, stdout, stderr).wait_for_output(self)
751 }
752
753 fn fail(&self, message: &str) -> ! {
754 println!("{message}");
755
756 if !self.is_verbose() {
757 println!("Command has failed. Rerun with -v to see more details.");
758 }
759 exit!(1);
760 }
761
762 pub fn stream(
766 &self,
767 command: &mut BootstrapCommand,
768 stdout: OutputMode,
769 stderr: OutputMode,
770 ) -> Option<StreamingCommand> {
771 command.mark_as_executed();
772 if !command.run_in_dry_run && self.dry_run() {
773 return None;
774 }
775
776 #[cfg(feature = "tracing")]
777 let span_guard = crate::utils::tracing::trace_cmd(command);
778
779 let start_time = Instant::now();
780 let fingerprint = command.fingerprint();
781 let cmd = &mut command.command;
782 cmd.stdout(stdout.stdio());
783 cmd.stderr(stderr.stdio());
784 let child = cmd.spawn();
785 let mut child = match child {
786 Ok(child) => child,
787 Err(e) => panic!("failed to execute command: {cmd:?}\nERROR: {e}"),
788 };
789
790 let stdout = child.stdout.take();
791 let stderr = child.stderr.take();
792 Some(StreamingCommand {
793 child,
794 stdout,
795 stderr,
796 fingerprint,
797 start_time,
798 #[cfg(feature = "tracing")]
799 _span_guard: span_guard,
800 })
801 }
802}
803
804impl AsRef<ExecutionContext> for ExecutionContext {
805 fn as_ref(&self) -> &ExecutionContext {
806 self
807 }
808}
809
810impl StreamingCommand {
811 pub fn wait(
812 mut self,
813 exec_ctx: impl AsRef<ExecutionContext>,
814 ) -> Result<ExitStatus, std::io::Error> {
815 let exec_ctx = exec_ctx.as_ref();
816 let output = self.child.wait();
817 exec_ctx.profiler().record_execution(self.fingerprint, self.start_time);
818 output
819 }
820}
821
822impl<'a> DeferredCommand<'a> {
823 pub fn wait_for_output(self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
824 match self.state {
825 CommandState::Cached(output) => output,
826 CommandState::Deferred {
827 process,
828 command,
829 stdout,
830 stderr,
831 executed_at,
832 fingerprint,
833 start_time,
834 #[cfg(feature = "tracing")]
835 _span_guard,
836 } => {
837 let exec_ctx = exec_ctx.as_ref();
838
839 let output =
840 Self::finish_process(process, command, stdout, stderr, executed_at, exec_ctx);
841
842 #[cfg(feature = "tracing")]
843 drop(_span_guard);
844
845 if (!exec_ctx.dry_run() || command.run_in_dry_run)
846 && output.status().is_some()
847 && command.should_cache
848 {
849 exec_ctx.command_cache.insert(fingerprint.clone(), output.clone());
850 exec_ctx.profiler.record_execution(fingerprint, start_time);
851 }
852
853 output
854 }
855 }
856 }
857
858 pub fn finish_process(
859 mut process: Option<Result<Child, std::io::Error>>,
860 command: &mut BootstrapCommand,
861 stdout: OutputMode,
862 stderr: OutputMode,
863 executed_at: &'a std::panic::Location<'a>,
864 exec_ctx: &ExecutionContext,
865 ) -> CommandOutput {
866 use std::fmt::Write;
867
868 command.mark_as_executed();
869
870 let process = match process.take() {
871 Some(p) => p,
872 None => return CommandOutput::default(),
873 };
874
875 let created_at = command.get_created_location();
876
877 #[allow(clippy::enum_variant_names)]
878 enum FailureReason {
879 FailedAtRuntime(ExitStatus),
880 FailedToFinish(std::io::Error),
881 FailedToStart(std::io::Error),
882 }
883
884 let (output, fail_reason) = match process {
885 Ok(child) => match child.wait_with_output() {
886 Ok(output) if output.status.success() => {
887 (CommandOutput::from_output(output, stdout, stderr), None)
889 }
890 Ok(output) => {
891 let status = output.status;
893 (
894 CommandOutput::from_output(output, stdout, stderr),
895 Some(FailureReason::FailedAtRuntime(status)),
896 )
897 }
898 Err(e) => {
899 (
901 CommandOutput::not_finished(stdout, stderr),
902 Some(FailureReason::FailedToFinish(e)),
903 )
904 }
905 },
906 Err(e) => {
907 (CommandOutput::not_finished(stdout, stderr), Some(FailureReason::FailedToStart(e)))
909 }
910 };
911
912 if let Some(fail_reason) = fail_reason {
913 let mut error_message = String::new();
914 let command_str = if exec_ctx.is_verbose() {
915 format!("{command:?}")
916 } else {
917 command.fingerprint().format_short_cmd()
918 };
919 let action = match fail_reason {
920 FailureReason::FailedAtRuntime(e) => {
921 format!("failed with exit code {}", e.code().unwrap_or(1))
922 }
923 FailureReason::FailedToFinish(e) => {
924 format!("failed to finish: {e:?}")
925 }
926 FailureReason::FailedToStart(e) => {
927 format!("failed to start: {e:?}")
928 }
929 };
930 writeln!(
931 error_message,
932 r#"Command `{command_str}` {action}
933Created at: {created_at}
934Executed at: {executed_at}"#,
935 )
936 .unwrap();
937 if stdout.captures() {
938 writeln!(error_message, "\n--- STDOUT vvv\n{}", output.stdout().trim()).unwrap();
939 }
940 if stderr.captures() {
941 writeln!(error_message, "\n--- STDERR vvv\n{}", output.stderr().trim()).unwrap();
942 }
943
944 match command.failure_behavior {
945 BehaviorOnFailure::DelayFail => {
946 if exec_ctx.fail_fast {
947 exec_ctx.fail(&error_message);
948 }
949 exec_ctx.add_to_delay_failure(error_message);
950 }
951 BehaviorOnFailure::Exit => {
952 exec_ctx.fail(&error_message);
953 }
954 BehaviorOnFailure::Ignore => {
955 }
959 }
960 }
961
962 output
963 }
964}