use std::ffi::OsStr;
use std::fmt::{Debug, Formatter};
use std::path::Path;
use std::process::{Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio};
use build_helper::ci::CiEnv;
use build_helper::drop_bomb::DropBomb;
use crate::Build;
#[derive(Debug, Copy, Clone)]
pub enum BehaviorOnFailure {
Exit,
DelayFail,
Ignore,
}
#[derive(Debug, Copy, Clone)]
pub enum OutputMode {
Print,
Capture,
}
impl OutputMode {
pub fn captures(&self) -> bool {
match self {
OutputMode::Print => false,
OutputMode::Capture => true,
}
}
pub fn stdio(&self) -> Stdio {
match self {
OutputMode::Print => Stdio::inherit(),
OutputMode::Capture => Stdio::piped(),
}
}
}
pub struct BootstrapCommand {
command: Command,
pub failure_behavior: BehaviorOnFailure,
pub run_always: bool,
drop_bomb: DropBomb,
}
impl BootstrapCommand {
#[track_caller]
pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
Command::new(program).into()
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.command.arg(arg.as_ref());
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.command.args(args);
self
}
pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.command.env(key, val);
self
}
pub fn get_envs(&self) -> CommandEnvs<'_> {
self.command.get_envs()
}
pub fn get_args(&self) -> CommandArgs<'_> {
self.command.get_args()
}
pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
self.command.env_remove(key);
self
}
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
self.command.current_dir(dir);
self
}
#[must_use]
pub fn delay_failure(self) -> Self {
Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
}
#[must_use]
pub fn fail_fast(self) -> Self {
Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
}
#[must_use]
pub fn allow_failure(self) -> Self {
Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
}
pub fn run_always(&mut self) -> &mut Self {
self.run_always = true;
self
}
#[track_caller]
pub fn run(&mut self, builder: &Build) -> bool {
builder.run(self, OutputMode::Print, OutputMode::Print).is_success()
}
#[track_caller]
pub fn run_capture(&mut self, builder: &Build) -> CommandOutput {
builder.run(self, OutputMode::Capture, OutputMode::Capture)
}
#[track_caller]
pub fn run_capture_stdout(&mut self, builder: &Build) -> CommandOutput {
builder.run(self, OutputMode::Capture, OutputMode::Print)
}
pub fn as_command_mut(&mut self) -> &mut Command {
self.mark_as_executed();
&mut self.command
}
pub fn mark_as_executed(&mut self) {
self.drop_bomb.defuse();
}
pub fn get_created_location(&self) -> std::panic::Location<'static> {
self.drop_bomb.get_created_location()
}
pub fn force_coloring_in_ci(&mut self) {
if CiEnv::is_ci() {
self.env("TERM", "xterm").args(["--color", "always"]);
}
}
}
impl Debug for BootstrapCommand {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.command)?;
write!(f, " (failure_mode={:?})", self.failure_behavior)
}
}
impl From<Command> for BootstrapCommand {
#[track_caller]
fn from(command: Command) -> Self {
let program = command.get_program().to_owned();
Self {
command,
failure_behavior: BehaviorOnFailure::Exit,
run_always: false,
drop_bomb: DropBomb::arm(program),
}
}
}
enum CommandStatus {
Finished(ExitStatus),
DidNotStart,
}
#[track_caller]
#[must_use]
pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
BootstrapCommand::new(program)
}
pub struct CommandOutput {
status: CommandStatus,
stdout: Option<Vec<u8>>,
stderr: Option<Vec<u8>>,
}
impl CommandOutput {
#[must_use]
pub fn did_not_start(stdout: OutputMode, stderr: OutputMode) -> Self {
Self {
status: CommandStatus::DidNotStart,
stdout: match stdout {
OutputMode::Print => None,
OutputMode::Capture => Some(vec![]),
},
stderr: match stderr {
OutputMode::Print => None,
OutputMode::Capture => Some(vec![]),
},
}
}
#[must_use]
pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
Self {
status: CommandStatus::Finished(output.status),
stdout: match stdout {
OutputMode::Print => None,
OutputMode::Capture => Some(output.stdout),
},
stderr: match stderr {
OutputMode::Print => None,
OutputMode::Capture => Some(output.stderr),
},
}
}
#[must_use]
pub fn is_success(&self) -> bool {
match self.status {
CommandStatus::Finished(status) => status.success(),
CommandStatus::DidNotStart => false,
}
}
#[must_use]
pub fn is_failure(&self) -> bool {
!self.is_success()
}
#[must_use]
pub fn status(&self) -> Option<ExitStatus> {
match self.status {
CommandStatus::Finished(status) => Some(status),
CommandStatus::DidNotStart => None,
}
}
#[must_use]
pub fn stdout(&self) -> String {
String::from_utf8(
self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
)
.expect("Cannot parse process stdout as UTF-8")
}
#[must_use]
pub fn stdout_if_present(&self) -> Option<String> {
self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
}
#[must_use]
pub fn stdout_if_ok(&self) -> Option<String> {
if self.is_success() { Some(self.stdout()) } else { None }
}
#[must_use]
pub fn stderr(&self) -> String {
String::from_utf8(
self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
)
.expect("Cannot parse process stderr as UTF-8")
}
#[must_use]
pub fn stderr_if_present(&self) -> Option<String> {
self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
}
}
impl Default for CommandOutput {
fn default() -> Self {
Self {
status: CommandStatus::Finished(ExitStatus::default()),
stdout: Some(vec![]),
stderr: Some(vec![]),
}
}
}