use crate::cli::ExecContext;
use crate::error::{Result, RsbuildError};
use crate::output::CommandResult;
use crate::progress::{self, finish_error, finish_success};
use colored::Colorize;
use std::io::{self, BufRead, Write};
use std::process::{Command, Output};
use std::time::Instant;
const PREFIX_RSBUILD: &str = "[rsbuild]";
const PREFIX_OUTPUT: &str = "[output]";
const PREFIX_ERROR: &str = "[error]";
const PREFIX_DRY_RUN: &str = "[dry-run]";
pub fn check_tool(name: &str) -> Result<()> {
which::which(name).map_err(|_| RsbuildError::ToolNotFound {
tool: name.to_string(),
hint: get_install_hint(name),
})?;
Ok(())
}
fn get_install_hint(tool: &str) -> String {
match tool {
"docker" => "Install Docker: https://docs.docker.com/get-docker/".to_string(),
"cargo" => "Install Rust: https://rustup.rs/".to_string(),
"pip" => "Install pip: https://pip.pypa.io/en/stable/installation/".to_string(),
"uv" => "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh".to_string(),
"task" => "Install Task: https://taskfile.dev/installation/".to_string(),
"cythonize" => "Install Cython: pip install cython".to_string(),
"rsync" => "Install rsync using your package manager".to_string(),
"glances" => "Install glances: pip install glances".to_string(),
_ => format!("Please install '{}' and ensure it's in your PATH", tool),
}
}
pub fn read_output(command: &str) -> Result<Output> {
Command::new("sh")
.arg("-c")
.arg(command)
.output()
.map_err(|e| RsbuildError::ExecutionFailed(e.to_string()))
}
pub fn read_output_str(command: &str) -> Result<String> {
let output = read_output(command)?;
let mut output_str = String::from_utf8(output.stdout)?;
if output_str.ends_with('\n') {
output_str.pop();
}
Ok(output_str)
}
pub fn exec(command: &str, ctx: &ExecContext) -> Result<String> {
if ctx.dry_run {
if ctx.should_print() {
println!(
"{} {}",
PREFIX_DRY_RUN.bold().cyan(),
command.italic()
);
}
return Ok(String::new());
}
if ctx.should_print() && !ctx.is_json() {
println!("{} `{}`", PREFIX_RSBUILD.bold().yellow(), command);
}
let output = read_output(command)?;
let status = output.status;
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
let stdout_clean = stdout
.replace(&format!("{} ", PREFIX_OUTPUT), "")
.replace(&format!("{} ", PREFIX_RSBUILD), "");
let stderr_clean = stderr
.replace(&format!("{} ", PREFIX_ERROR), "")
.replace(&format!("{} ", PREFIX_RSBUILD), "");
if (ctx.should_print() || ctx.verbose) && !ctx.is_json() {
if !stdout_clean.is_empty() {
let formatted = format!("{} {}", PREFIX_OUTPUT.bold().blue(), stdout_clean);
io::stdout().write_all(formatted.as_bytes())?;
}
if !stderr_clean.is_empty() {
let formatted = format!("{} {}", PREFIX_ERROR.bold().red(), stderr_clean);
io::stderr().write_all(formatted.as_bytes())?;
}
}
if !status.success() {
return Err(RsbuildError::CommandFailed {
command: command.to_string(),
code: status.code().unwrap_or(-1),
message: stderr_clean.lines().next().unwrap_or("Unknown error").to_string(),
});
}
Ok(stdout_clean)
}
pub fn exec_with_progress(
command: &str,
ctx: &ExecContext,
message: impl Into<String>,
) -> Result<String> {
let msg = message.into();
let spinner = progress::create_spinner(ctx, &msg);
let start = Instant::now();
let result = exec_internal(command, ctx);
let duration = start.elapsed();
match &result {
Ok(_) => {
finish_success(spinner, format!("{} ({:.2}s)", msg, duration.as_secs_f64()));
}
Err(e) => {
finish_error(spinner, format!("{}: {}", msg, e));
}
}
result
}
fn exec_internal(command: &str, ctx: &ExecContext) -> Result<String> {
if ctx.dry_run {
return Ok(String::new());
}
let output = read_output(command)?;
let status = output.status;
let stdout = String::from_utf8(output.stdout)?;
let stderr = String::from_utf8(output.stderr)?;
let stdout_clean = stdout
.replace(&format!("{} ", PREFIX_OUTPUT), "")
.replace(&format!("{} ", PREFIX_RSBUILD), "");
let stderr_clean = stderr
.replace(&format!("{} ", PREFIX_ERROR), "")
.replace(&format!("{} ", PREFIX_RSBUILD), "");
if !status.success() {
return Err(RsbuildError::CommandFailed {
command: command.to_string(),
code: status.code().unwrap_or(-1),
message: stderr_clean.lines().next().unwrap_or("Unknown error").to_string(),
});
}
Ok(stdout_clean)
}
pub fn exec_for_json(command: &str, ctx: &ExecContext) -> CommandResult {
let start = Instant::now();
let result = exec_internal(command, ctx);
let duration = start.elapsed();
match result {
Ok(output) => CommandResult::success(
command,
duration,
if output.is_empty() { None } else { Some(output) },
),
Err(e) => CommandResult::failed(command, duration, e.to_string()),
}
}
pub fn exec_silent(command: &str, ctx: &ExecContext) -> Result<String> {
if ctx.dry_run {
return Ok(String::new());
}
let output = read_output(command)?;
let stdout = String::from_utf8(output.stdout)?;
if !output.status.success() {
let stderr = String::from_utf8(output.stderr)?;
return Err(RsbuildError::CommandFailed {
command: command.to_string(),
code: output.status.code().unwrap_or(-1),
message: stderr.lines().next().unwrap_or("Unknown error").to_string(),
});
}
Ok(stdout)
}
pub fn exec_commands(commands: &[&str], ctx: &ExecContext) -> Result<()> {
for command in commands {
exec(command, ctx)?;
}
Ok(())
}
pub fn exec_ignore_error(command: &str, ctx: &ExecContext) {
let _ = exec_silent(command, ctx);
}
pub fn print_status(message: &str, ctx: &ExecContext) {
if ctx.should_print() && !ctx.is_json() {
println!("{} {}", PREFIX_RSBUILD.bold().green(), message);
}
}
pub fn print_warning(message: &str, ctx: &ExecContext) {
if ctx.should_print() && !ctx.is_json() {
eprintln!("{} {}", "[warning]".bold().yellow(), message);
}
}
pub fn confirm(message: &str, ctx: &ExecContext) -> bool {
if ctx.yes || ctx.dry_run {
return true;
}
print!("{} {} [y/N] ", PREFIX_RSBUILD.bold().cyan(), message);
io::stdout().flush().ok();
let stdin = io::stdin();
let mut line = String::new();
if stdin.lock().read_line(&mut line).is_ok() {
let response = line.trim().to_lowercase();
return response == "y" || response == "yes";
}
false
}
pub fn confirm_overwrite(path: &std::path::Path, ctx: &ExecContext) -> bool {
if !path.exists() {
return true;
}
if ctx.yes || ctx.dry_run {
if ctx.should_print() {
println!(
"{} Overwriting: {}",
PREFIX_RSBUILD.bold().yellow(),
path.display()
);
}
return true;
}
confirm(&format!("Overwrite {}?", path.display()), ctx)
}