use std::process::Output;
use std::time::Instant;
use tokio::process::Command;
use tracing::{debug, info};
use crate::logging::{log_timed, LogLevel};
use crate::utils::command_exists;
pub async fn run_setup(debug: bool) -> Result<(), String> {
let (program, args, description) = build_setup_command()?;
if debug {
debug!("Running setup command: {} {}", program, args.join(" "));
}
let start: Instant = Instant::now();
let output: Output = Command::new(&program)
.args(&args)
.output()
.await
.map_err(|e| format!("Failed to execute setup command: {}", e))?;
let elapsed = start.elapsed();
if debug {
debug!("Setup command output: {:?}", output);
debug!("Setup command took: {:.2?}", elapsed);
}
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let details = if !stderr.is_empty() { stderr } else { stdout };
return Err(format!(
"Setup failed while running '{} {}': {}",
program,
args.join(" "),
details
));
}
let _ = log_timed(
LogLevel::Success,
"setup",
&format!("Setup completed with {}", description),
elapsed.as_millis() as u64,
)
.await;
if !output.stdout.is_empty() {
info!("Setup output: {}", String::from_utf8_lossy(&output.stdout));
}
info!("Setup completed successfully!");
Ok(())
}
fn build_setup_command() -> Result<(String, Vec<String>, String), String> {
let target_os = if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unsupported"
};
build_setup_command_for(target_os, command_exists)
}
fn build_setup_command_for<F>(
target_os: &str,
command_exists: F,
) -> Result<(String, Vec<String>, String), String>
where
F: Fn(&str) -> bool,
{
if target_os == "macos" {
if !command_exists("brew") {
return Err(
"Homebrew is required on macOS. Install it first from https://brew.sh/."
.to_string(),
);
}
return Ok((
"brew".to_string(),
vec![
"install".to_string(),
"nginx".to_string(),
"pkg-config".to_string(),
"openssl@3".to_string(),
"findutils".to_string(),
"inetutils".to_string(),
"certbot".to_string(),
"python".to_string(),
],
"Homebrew".to_string(),
));
}
if target_os == "linux" {
let package_manager = if command_exists("apt-get") {
"apt-get"
} else if command_exists("apt") {
"apt"
} else {
return Err(
"Unsupported Linux package manager. Install dependencies manually or add apt/apt-get.".to_string(),
);
};
let mut args = Vec::new();
let program = if command_exists("sudo") {
args.push(package_manager.to_string());
"sudo".to_string()
} else {
package_manager.to_string()
};
args.extend(
[
"install",
"-y",
"net-tools",
"nginx",
"pkg-config",
"libssl-dev",
"build-essential",
"plocate",
"sshpass",
"neofetch",
"certbot",
"python3-certbot-nginx",
]
.into_iter()
.map(str::to_string),
);
return Ok((program, args, package_manager.to_string()));
}
Err("Setup is currently supported on Linux (apt) and macOS (Homebrew).".to_string())
}
#[cfg(test)]
mod tests {
use super::build_setup_command_for;
#[test]
fn linux_prefers_apt_get_and_sudo_when_available() {
let command = build_setup_command_for("linux", |cmd| matches!(cmd, "apt-get" | "sudo"))
.expect("linux setup command should build");
assert_eq!(command.0, "sudo");
assert_eq!(command.1[0], "apt-get");
assert_eq!(command.1[1], "install");
assert_eq!(command.2, "apt-get");
}
#[test]
fn linux_falls_back_to_apt_without_sudo() {
let command =
build_setup_command_for("linux", |cmd| cmd == "apt").expect("apt should be accepted");
assert_eq!(command.0, "apt");
assert_eq!(command.1[0], "install");
assert_eq!(command.2, "apt");
}
#[test]
fn linux_errors_without_supported_package_manager() {
let error = build_setup_command_for("linux", |_| false)
.expect_err("linux setup should fail without apt");
assert!(error.contains("Unsupported Linux package manager"));
}
#[test]
fn macos_requires_brew() {
let error = build_setup_command_for("macos", |_| false)
.expect_err("macOS setup should require brew");
assert!(error.contains("Homebrew is required on macOS"));
}
#[test]
fn macos_uses_brew_install_command() {
let command =
build_setup_command_for("macos", |cmd| cmd == "brew").expect("brew should be accepted");
assert_eq!(command.0, "brew");
assert_eq!(command.1[0], "install");
assert!(command.1.iter().any(|arg| arg == "python"));
assert_eq!(command.2, "Homebrew");
}
}