1use std::collections::HashMap;
11use std::ffi::{OsStr, OsString};
12use std::fmt::{Debug, Formatter};
13use std::hash::Hash;
14use std::panic::Location;
15use std::path::Path;
16use std::process::{Child, Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio};
17use std::sync::{Arc, Mutex};
18
19use build_helper::ci::CiEnv;
20use build_helper::drop_bomb::DropBomb;
21use build_helper::exit;
22
23use crate::PathBuf;
24use crate::core::config::DryRun;
25#[cfg(feature = "tracing")]
26use crate::trace_cmd;
27
28#[derive(Debug, Copy, Clone)]
30pub enum BehaviorOnFailure {
31 Exit,
33 DelayFail,
35 Ignore,
37}
38
39#[derive(Debug, Copy, Clone)]
42pub enum OutputMode {
43 Print,
45 Capture,
47}
48
49impl OutputMode {
50 pub fn captures(&self) -> bool {
51 match self {
52 OutputMode::Print => false,
53 OutputMode::Capture => true,
54 }
55 }
56
57 pub fn stdio(&self) -> Stdio {
58 match self {
59 OutputMode::Print => Stdio::inherit(),
60 OutputMode::Capture => Stdio::piped(),
61 }
62 }
63}
64
65#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
66pub struct CommandCacheKey {
67 program: OsString,
68 args: Vec<OsString>,
69 envs: Vec<(OsString, Option<OsString>)>,
70 cwd: Option<PathBuf>,
71}
72
73pub struct BootstrapCommand {
90 command: Command,
91 pub failure_behavior: BehaviorOnFailure,
92 pub run_in_dry_run: bool,
94 drop_bomb: DropBomb,
97 should_cache: bool,
98}
99
100impl<'a> BootstrapCommand {
101 #[track_caller]
102 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
103 Command::new(program).into()
104 }
105 pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
106 self.command.arg(arg.as_ref());
107 self
108 }
109
110 pub fn do_not_cache(&mut self) -> &mut Self {
111 self.should_cache = false;
112 self
113 }
114
115 pub fn args<I, S>(&mut self, args: I) -> &mut Self
116 where
117 I: IntoIterator<Item = S>,
118 S: AsRef<OsStr>,
119 {
120 self.command.args(args);
121 self
122 }
123
124 pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
125 where
126 K: AsRef<OsStr>,
127 V: AsRef<OsStr>,
128 {
129 self.command.env(key, val);
130 self
131 }
132
133 pub fn get_envs(&self) -> CommandEnvs<'_> {
134 self.command.get_envs()
135 }
136
137 pub fn get_args(&self) -> CommandArgs<'_> {
138 self.command.get_args()
139 }
140
141 pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
142 self.command.env_remove(key);
143 self
144 }
145
146 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
147 self.command.current_dir(dir);
148 self
149 }
150
151 pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
152 self.command.stdin(stdin);
153 self
154 }
155
156 #[must_use]
157 pub fn delay_failure(self) -> Self {
158 Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
159 }
160
161 pub fn fail_fast(self) -> Self {
162 Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
163 }
164
165 #[must_use]
166 pub fn allow_failure(self) -> Self {
167 Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
168 }
169
170 pub fn run_in_dry_run(&mut self) -> &mut Self {
171 self.run_in_dry_run = true;
172 self
173 }
174
175 #[track_caller]
178 pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
179 exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
180 }
181
182 #[track_caller]
184 pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
185 exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
186 }
187
188 #[track_caller]
190 pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
191 exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
192 }
193
194 #[track_caller]
196 pub fn start_capture(
197 &'a mut self,
198 exec_ctx: impl AsRef<ExecutionContext>,
199 ) -> DeferredCommand<'a> {
200 exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
201 }
202
203 #[track_caller]
205 pub fn start_capture_stdout(
206 &'a mut self,
207 exec_ctx: impl AsRef<ExecutionContext>,
208 ) -> DeferredCommand<'a> {
209 exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
210 }
211
212 pub fn as_command_mut(&mut self) -> &mut Command {
215 self.mark_as_executed();
219 self.do_not_cache();
220 &mut self.command
221 }
222
223 pub fn mark_as_executed(&mut self) {
226 self.drop_bomb.defuse();
227 }
228
229 pub fn get_created_location(&self) -> std::panic::Location<'static> {
231 self.drop_bomb.get_created_location()
232 }
233
234 pub fn force_coloring_in_ci(&mut self) {
236 if CiEnv::is_ci() {
237 self.env("TERM", "xterm").args(["--color", "always"]);
243 }
244 }
245
246 pub fn cache_key(&self) -> Option<CommandCacheKey> {
247 if !self.should_cache {
248 return None;
249 }
250 let command = &self.command;
251 Some(CommandCacheKey {
252 program: command.get_program().into(),
253 args: command.get_args().map(OsStr::to_os_string).collect(),
254 envs: command
255 .get_envs()
256 .map(|(k, v)| (k.to_os_string(), v.map(|val| val.to_os_string())))
257 .collect(),
258 cwd: command.get_current_dir().map(Path::to_path_buf),
259 })
260 }
261}
262
263impl Debug for BootstrapCommand {
264 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265 write!(f, "{:?}", self.command)?;
266 write!(f, " (failure_mode={:?})", self.failure_behavior)
267 }
268}
269
270impl From<Command> for BootstrapCommand {
271 #[track_caller]
272 fn from(command: Command) -> Self {
273 let program = command.get_program().to_owned();
274 Self {
275 should_cache: true,
276 command,
277 failure_behavior: BehaviorOnFailure::Exit,
278 run_in_dry_run: false,
279 drop_bomb: DropBomb::arm(program),
280 }
281 }
282}
283
284#[derive(Clone, PartialEq)]
286enum CommandStatus {
287 Finished(ExitStatus),
289 DidNotStart,
291}
292
293#[track_caller]
296#[must_use]
297pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
298 BootstrapCommand::new(program)
299}
300
301#[derive(Clone, PartialEq)]
303pub struct CommandOutput {
304 status: CommandStatus,
305 stdout: Option<Vec<u8>>,
306 stderr: Option<Vec<u8>>,
307}
308
309impl CommandOutput {
310 #[must_use]
311 pub fn did_not_start(stdout: OutputMode, stderr: OutputMode) -> Self {
312 Self {
313 status: CommandStatus::DidNotStart,
314 stdout: match stdout {
315 OutputMode::Print => None,
316 OutputMode::Capture => Some(vec![]),
317 },
318 stderr: match stderr {
319 OutputMode::Print => None,
320 OutputMode::Capture => Some(vec![]),
321 },
322 }
323 }
324
325 #[must_use]
326 pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
327 Self {
328 status: CommandStatus::Finished(output.status),
329 stdout: match stdout {
330 OutputMode::Print => None,
331 OutputMode::Capture => Some(output.stdout),
332 },
333 stderr: match stderr {
334 OutputMode::Print => None,
335 OutputMode::Capture => Some(output.stderr),
336 },
337 }
338 }
339
340 #[must_use]
341 pub fn is_success(&self) -> bool {
342 match self.status {
343 CommandStatus::Finished(status) => status.success(),
344 CommandStatus::DidNotStart => false,
345 }
346 }
347
348 #[must_use]
349 pub fn is_failure(&self) -> bool {
350 !self.is_success()
351 }
352
353 pub fn status(&self) -> Option<ExitStatus> {
354 match self.status {
355 CommandStatus::Finished(status) => Some(status),
356 CommandStatus::DidNotStart => None,
357 }
358 }
359
360 #[must_use]
361 pub fn stdout(&self) -> String {
362 String::from_utf8(
363 self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
364 )
365 .expect("Cannot parse process stdout as UTF-8")
366 }
367
368 #[must_use]
369 pub fn stdout_if_present(&self) -> Option<String> {
370 self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
371 }
372
373 #[must_use]
374 pub fn stdout_if_ok(&self) -> Option<String> {
375 if self.is_success() { Some(self.stdout()) } else { None }
376 }
377
378 #[must_use]
379 pub fn stderr(&self) -> String {
380 String::from_utf8(
381 self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
382 )
383 .expect("Cannot parse process stderr as UTF-8")
384 }
385
386 #[must_use]
387 pub fn stderr_if_present(&self) -> Option<String> {
388 self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
389 }
390}
391
392impl Default for CommandOutput {
393 fn default() -> Self {
394 Self {
395 status: CommandStatus::Finished(ExitStatus::default()),
396 stdout: Some(vec![]),
397 stderr: Some(vec![]),
398 }
399 }
400}
401
402#[cfg(feature = "tracing")]
405pub trait FormatShortCmd {
406 fn format_short_cmd(&self) -> String;
407}
408
409#[cfg(feature = "tracing")]
410impl FormatShortCmd for BootstrapCommand {
411 fn format_short_cmd(&self) -> String {
412 self.command.format_short_cmd()
413 }
414}
415
416#[cfg(feature = "tracing")]
417impl FormatShortCmd for Command {
418 fn format_short_cmd(&self) -> String {
419 let program = Path::new(self.get_program());
420 let mut line = vec![program.file_name().unwrap().to_str().unwrap()];
421 line.extend(self.get_args().map(|arg| arg.to_str().unwrap()));
422 line.join(" ")
423 }
424}
425
426#[derive(Clone, Default)]
427pub struct ExecutionContext {
428 dry_run: DryRun,
429 verbose: u8,
430 pub fail_fast: bool,
431 delayed_failures: Arc<Mutex<Vec<String>>>,
432 command_cache: Arc<CommandCache>,
433}
434
435#[derive(Default)]
436pub struct CommandCache {
437 cache: Mutex<HashMap<CommandCacheKey, CommandOutput>>,
438}
439
440enum CommandState<'a> {
441 Cached(CommandOutput),
442 Deferred {
443 process: Option<Result<Child, std::io::Error>>,
444 command: &'a mut BootstrapCommand,
445 stdout: OutputMode,
446 stderr: OutputMode,
447 executed_at: &'a Location<'a>,
448 cache_key: Option<CommandCacheKey>,
449 },
450}
451
452#[must_use]
453pub struct DeferredCommand<'a> {
454 state: CommandState<'a>,
455}
456
457impl CommandCache {
458 pub fn get(&self, key: &CommandCacheKey) -> Option<CommandOutput> {
459 self.cache.lock().unwrap().get(key).cloned()
460 }
461
462 pub fn insert(&self, key: CommandCacheKey, output: CommandOutput) {
463 self.cache.lock().unwrap().insert(key, output);
464 }
465}
466
467impl ExecutionContext {
468 pub fn new() -> Self {
469 ExecutionContext::default()
470 }
471
472 pub fn dry_run(&self) -> bool {
473 match self.dry_run {
474 DryRun::Disabled => false,
475 DryRun::SelfCheck | DryRun::UserSelected => true,
476 }
477 }
478
479 pub fn get_dry_run(&self) -> &DryRun {
480 &self.dry_run
481 }
482
483 pub fn verbose(&self, f: impl Fn()) {
484 if self.is_verbose() {
485 f()
486 }
487 }
488
489 pub fn is_verbose(&self) -> bool {
490 self.verbose > 0
491 }
492
493 pub fn fail_fast(&self) -> bool {
494 self.fail_fast
495 }
496
497 pub fn set_dry_run(&mut self, value: DryRun) {
498 self.dry_run = value;
499 }
500
501 pub fn set_verbose(&mut self, value: u8) {
502 self.verbose = value;
503 }
504
505 pub fn set_fail_fast(&mut self, value: bool) {
506 self.fail_fast = value;
507 }
508
509 pub fn add_to_delay_failure(&self, message: String) {
510 self.delayed_failures.lock().unwrap().push(message);
511 }
512
513 pub fn report_failures_and_exit(&self) {
514 let failures = self.delayed_failures.lock().unwrap();
515 if failures.is_empty() {
516 return;
517 }
518 eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
519 for failure in &*failures {
520 eprintln!(" - {failure}");
521 }
522 exit!(1);
523 }
524
525 #[track_caller]
529 pub fn start<'a>(
530 &self,
531 command: &'a mut BootstrapCommand,
532 stdout: OutputMode,
533 stderr: OutputMode,
534 ) -> DeferredCommand<'a> {
535 let cache_key = command.cache_key();
536
537 if let Some(cached_output) = cache_key.as_ref().and_then(|key| self.command_cache.get(key))
538 {
539 command.mark_as_executed();
540 self.verbose(|| println!("Cache hit: {command:?}"));
541 return DeferredCommand { state: CommandState::Cached(cached_output) };
542 }
543
544 let created_at = command.get_created_location();
545 let executed_at = std::panic::Location::caller();
546
547 if self.dry_run() && !command.run_in_dry_run {
548 return DeferredCommand {
549 state: CommandState::Deferred {
550 process: None,
551 command,
552 stdout,
553 stderr,
554 executed_at,
555 cache_key,
556 },
557 };
558 }
559
560 #[cfg(feature = "tracing")]
561 let _run_span = trace_cmd!(command);
562
563 self.verbose(|| {
564 println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
565 });
566
567 let cmd = &mut command.command;
568 cmd.stdout(stdout.stdio());
569 cmd.stderr(stderr.stdio());
570
571 let child = cmd.spawn();
572
573 DeferredCommand {
574 state: CommandState::Deferred {
575 process: Some(child),
576 command,
577 stdout,
578 stderr,
579 executed_at,
580 cache_key,
581 },
582 }
583 }
584
585 #[track_caller]
589 pub fn run(
590 &self,
591 command: &mut BootstrapCommand,
592 stdout: OutputMode,
593 stderr: OutputMode,
594 ) -> CommandOutput {
595 self.start(command, stdout, stderr).wait_for_output(self)
596 }
597
598 fn fail(&self, message: &str, output: CommandOutput) -> ! {
599 if self.is_verbose() {
600 println!("{message}");
601 } else {
602 let (stdout, stderr) = (output.stdout_if_present(), output.stderr_if_present());
603 if stdout.is_some() || stderr.is_some() {
607 if let Some(stdout) = output.stdout_if_present().take_if(|s| !s.trim().is_empty()) {
608 println!("STDOUT:\n{stdout}\n");
609 }
610 if let Some(stderr) = output.stderr_if_present().take_if(|s| !s.trim().is_empty()) {
611 println!("STDERR:\n{stderr}\n");
612 }
613 println!("Command has failed. Rerun with -v to see more details.");
614 } else {
615 println!("Command has failed. Rerun with -v to see more details.");
616 }
617 }
618 exit!(1);
619 }
620}
621
622impl AsRef<ExecutionContext> for ExecutionContext {
623 fn as_ref(&self) -> &ExecutionContext {
624 self
625 }
626}
627
628impl<'a> DeferredCommand<'a> {
629 pub fn wait_for_output(self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
630 match self.state {
631 CommandState::Cached(output) => output,
632 CommandState::Deferred { process, command, stdout, stderr, executed_at, cache_key } => {
633 let exec_ctx = exec_ctx.as_ref();
634
635 let output =
636 Self::finish_process(process, command, stdout, stderr, executed_at, exec_ctx);
637
638 if (!exec_ctx.dry_run() || command.run_in_dry_run)
639 && let (Some(cache_key), Some(_)) = (&cache_key, output.status())
640 {
641 exec_ctx.command_cache.insert(cache_key.clone(), output.clone());
642 }
643
644 output
645 }
646 }
647 }
648
649 pub fn finish_process(
650 mut process: Option<Result<Child, std::io::Error>>,
651 command: &mut BootstrapCommand,
652 stdout: OutputMode,
653 stderr: OutputMode,
654 executed_at: &'a std::panic::Location<'a>,
655 exec_ctx: &ExecutionContext,
656 ) -> CommandOutput {
657 command.mark_as_executed();
658
659 let process = match process.take() {
660 Some(p) => p,
661 None => return CommandOutput::default(),
662 };
663
664 let created_at = command.get_created_location();
665
666 let mut message = String::new();
667
668 let output = match process {
669 Ok(child) => match child.wait_with_output() {
670 Ok(result) if result.status.success() => {
671 CommandOutput::from_output(result, stdout, stderr)
673 }
674 Ok(result) => {
675 use std::fmt::Write;
677
678 writeln!(
679 message,
680 r#"
681Command {command:?} did not execute successfully.
682Expected success, got {}
683Created at: {created_at}
684Executed at: {executed_at}"#,
685 result.status,
686 )
687 .unwrap();
688
689 let output = CommandOutput::from_output(result, stdout, stderr);
690
691 if stdout.captures() {
692 writeln!(message, "\nSTDOUT ----\n{}", output.stdout().trim()).unwrap();
693 }
694 if stderr.captures() {
695 writeln!(message, "\nSTDERR ----\n{}", output.stderr().trim()).unwrap();
696 }
697
698 output
699 }
700 Err(e) => {
701 use std::fmt::Write;
703
704 writeln!(
705 message,
706 "\n\nCommand {command:?} did not execute successfully.\
707 \nIt was not possible to execute the command: {e:?}"
708 )
709 .unwrap();
710
711 CommandOutput::did_not_start(stdout, stderr)
712 }
713 },
714 Err(e) => {
715 use std::fmt::Write;
717
718 writeln!(
719 message,
720 "\n\nCommand {command:?} did not execute successfully.\
721 \nIt was not possible to execute the command: {e:?}"
722 )
723 .unwrap();
724
725 CommandOutput::did_not_start(stdout, stderr)
726 }
727 };
728
729 if !output.is_success() {
730 match command.failure_behavior {
731 BehaviorOnFailure::DelayFail => {
732 if exec_ctx.fail_fast {
733 exec_ctx.fail(&message, output);
734 }
735 exec_ctx.add_to_delay_failure(message);
736 }
737 BehaviorOnFailure::Exit => {
738 exec_ctx.fail(&message, output);
739 }
740 BehaviorOnFailure::Ignore => {
741 }
745 }
746 }
747
748 output
749 }
750}