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