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