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