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