use std::fmt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use crate::Result;
use crate::error::CliError;
pub mod abstract_runner;
pub mod git_runner;
pub mod npm_runner;
pub mod pnpm_runner;
pub mod runner;
pub mod runner_factory;
pub mod schematic_runner;
pub mod yarn_runner;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum RunnerKind {
Schematic,
Npm,
Yarn,
Pnpm,
Git,
Cargo,
}
impl RunnerKind {
pub const fn binary(self) -> &'static str {
match self {
Self::Schematic => "node",
Self::Npm => "npm",
Self::Yarn => "yarn",
Self::Pnpm => "pnpm",
Self::Git => "git",
Self::Cargo => "cargo",
}
}
}
impl fmt::Display for RunnerKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Schematic => "schematic",
Self::Npm => "npm",
Self::Yarn => "yarn",
Self::Pnpm => "pnpm",
Self::Git => "git",
Self::Cargo => "cargo",
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RunnerCommand {
pub binary: String,
pub prefix_args: Vec<String>,
pub command: String,
pub collect: bool,
pub cwd: Option<PathBuf>,
pub shell: bool,
pub env: Vec<(String, String)>,
}
impl RunnerCommand {
pub fn raw_full_command(&self) -> String {
let mut parts = Vec::with_capacity(2 + self.prefix_args.len());
parts.push(self.binary.as_str());
parts.extend(self.prefix_args.iter().map(String::as_str));
parts.push(self.command.as_str());
parts.join(" ")
}
pub fn execute(&self) -> Result<RunnerExecution> {
let command_line = self.command_line_for_execution();
let mut command = if self.shell {
shell_command(&command_line)
} else {
let mut command = Command::new(&self.binary);
command.args(&self.prefix_args).arg(&self.command);
command
};
if let Some(cwd) = &self.cwd {
command.current_dir(cwd);
}
command.envs(self.env.iter().map(|(key, value)| (key, value)));
if self.collect {
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = command.output()?;
let stdout =
strip_one_line_ending(String::from_utf8_lossy(&output.stdout).into_owned());
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
if output.status.success() {
Ok(RunnerExecution {
status: output.status,
stdout: Some(stdout),
stderr,
})
} else {
Err(CliError::RunnerFailed {
command: self.raw_full_command(),
reason: failure_reason(output.status, stderr),
})
}
} else {
command
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = command.status()?;
if status.success() {
Ok(RunnerExecution {
status,
stdout: None,
stderr: String::new(),
})
} else {
Err(CliError::RunnerFailed {
command: self.raw_full_command(),
reason: failure_reason(status, String::new()),
})
}
}
}
pub fn run(&self) -> Result<Option<String>> {
Ok(self.execute()?.stdout)
}
fn command_line_for_execution(&self) -> String {
let mut parts = Vec::with_capacity(2 + self.prefix_args.len());
parts.push(quote_shell_part(&self.binary));
parts.extend(self.prefix_args.iter().map(|arg| quote_shell_part(arg)));
parts.push(self.command.clone());
parts.join(" ")
}
}
#[derive(Debug)]
pub struct RunnerExecution {
pub status: ExitStatus,
pub stdout: Option<String>,
pub stderr: String,
}
impl RunnerExecution {
pub fn success(&self) -> bool {
self.status.success()
}
}
pub trait Runner {
fn kind(&self) -> RunnerKind;
fn binary(&self) -> &str;
fn prefix_args(&self) -> &[String];
fn describe(
&self,
command: impl Into<String>,
collect: bool,
cwd: Option<PathBuf>,
) -> RunnerCommand {
RunnerCommand {
binary: self.binary().to_owned(),
prefix_args: self.prefix_args().to_vec(),
command: command.into(),
collect,
cwd,
shell: true,
env: Vec::new(),
}
}
fn raw_full_command(&self, command: impl AsRef<str>) -> String {
self.describe(command.as_ref(), false, None)
.raw_full_command()
}
fn execute(
&self,
command: impl Into<String>,
collect: bool,
cwd: Option<PathBuf>,
) -> Result<RunnerExecution> {
self.describe(command, collect, cwd).execute()
}
fn run(
&self,
command: impl Into<String>,
collect: bool,
cwd: Option<PathBuf>,
) -> Result<Option<String>> {
self.describe(command, collect, cwd).run()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProcessRunner {
kind: RunnerKind,
binary: String,
prefix_args: Vec<String>,
}
impl ProcessRunner {
pub fn new(kind: RunnerKind) -> Self {
Self {
kind,
binary: kind.binary().to_owned(),
prefix_args: Vec::new(),
}
}
pub fn with_binary(kind: RunnerKind, binary: impl Into<String>) -> Self {
Self {
kind,
binary: binary.into(),
prefix_args: Vec::new(),
}
}
pub fn schematic(schematics_binary: impl AsRef<Path>) -> Self {
Self {
kind: RunnerKind::Schematic,
binary: RunnerKind::Schematic.binary().to_owned(),
prefix_args: vec![quote_path(schematics_binary.as_ref())],
}
}
}
impl Runner for ProcessRunner {
fn kind(&self) -> RunnerKind {
self.kind
}
fn binary(&self) -> &str {
&self.binary
}
fn prefix_args(&self) -> &[String] {
&self.prefix_args
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct RunnerFactory;
impl RunnerFactory {
pub fn create(kind: RunnerKind) -> ProcessRunner {
ProcessRunner::new(kind)
}
pub fn create_schematic(schematics_binary: impl AsRef<Path>) -> ProcessRunner {
ProcessRunner::schematic(schematics_binary)
}
}
fn quote_path(path: &Path) -> String {
format!("\"{}\"", path.display())
}
fn shell_command(command_line: &str) -> Command {
#[cfg(windows)]
{
let mut command =
Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
command.arg("/C").arg(command_line);
command
}
#[cfg(not(windows))]
{
let mut command = Command::new("sh");
command.arg("-c").arg(command_line);
command
}
}
fn quote_shell_part(part: &str) -> String {
if part.is_empty()
|| part.starts_with('"')
|| part.starts_with('\'')
|| !part.chars().any(char::is_whitespace)
{
return part.to_owned();
}
#[cfg(windows)]
{
format!("\"{}\"", part.replace('"', "\\\""))
}
#[cfg(not(windows))]
{
format!("'{}'", part.replace('\'', "'\\''"))
}
}
fn strip_one_line_ending(mut value: String) -> String {
if value.ends_with("\r\n") {
value.truncate(value.len() - 2);
} else if value.ends_with('\n') {
value.truncate(value.len() - 1);
}
value
}
fn failure_reason(status: ExitStatus, stderr: String) -> String {
let status = match status.code() {
Some(code) => format!("process exited with status code {code}"),
None => "process terminated by signal".to_owned(),
};
let stderr = stderr.trim();
if stderr.is_empty() {
status
} else {
format!("{status}: {stderr}")
}
}