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