use crate::installers::common::utils::{
command_exists, print_error, print_step, print_success, print_warning,
};
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct InstallConfig {
pub repo_dir: String,
pub repo_url: String,
pub skip_build: bool,
pub branch: String,
}
impl Default for InstallConfig {
fn default() -> Self {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
Self {
repo_dir: std::env::var("REPO_DIR")
.unwrap_or_else(|_| format!("{home}/Stan-s-ML-Stack")),
repo_url: "https://github.com/scooter-lacroix/Stan-s-ML-Stack.git".to_string(),
skip_build: false,
branch: std::env::var("BRANCH").unwrap_or_else(|_| "main".to_string()),
}
}
}
#[derive(Debug, Clone)]
pub struct InstallResult {
pub success: bool,
pub binary_path: Option<String>,
pub repo_dir: String,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl InstallResult {
pub fn success(repo_dir: String, binary_path: String) -> Self {
Self {
success: true,
binary_path: Some(binary_path),
repo_dir,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn failure(error: String) -> Self {
Self {
success: false,
binary_path: None,
repo_dir: String::new(),
errors: vec![error],
warnings: Vec::new(),
}
}
}
pub fn install_sh(config: &InstallConfig) -> InstallResult {
let mut result = InstallResult {
success: false,
binary_path: None,
repo_dir: config.repo_dir.clone(),
errors: Vec::new(),
warnings: Vec::new(),
};
print_step("Checking dependencies...");
if !command_exists("git") {
let msg = "git is not installed. Please install git first.".to_string();
print_error(&msg);
result.errors.push(msg);
return result;
}
print_success("git found");
if !command_exists("rustc") || !command_exists("cargo") {
print_warning("Rust toolchain not found.");
print_step("Attempting to install Rust via rustup...");
if !command_exists("curl") {
let msg = "curl is required to install Rust. Please install curl first.".to_string();
print_error(&msg);
result.errors.push(msg);
return result;
}
match install_rust_toolchain() {
Ok(()) => print_success("Rust toolchain installed"),
Err(e) => {
let msg = format!("Failed to install Rust toolchain: {e}");
print_error(&msg);
result.errors.push(msg);
return result;
}
}
} else if let Ok(version) = get_rust_version() {
print_success(&format!("Rust toolchain found: {version}"));
}
let repo_path = PathBuf::from(&config.repo_dir);
if repo_path.join(".git").exists() {
print_step(&format!(
"Repository exists at {}. Updating...",
config.repo_dir
));
if let Err(e) = update_repo(&config.repo_dir, &config.branch) {
result
.warnings
.push(format!("Failed to update repository: {e}"));
print_warning(&format!("Repository update failed: {e}"));
} else {
print_success("Repository updated");
}
} else {
print_step(&format!("Cloning repository from {}...", config.repo_url));
if let Err(e) = clone_repo(&config.repo_url, &config.repo_dir, &config.branch) {
let msg = format!("Failed to clone repository: {e}");
print_error(&msg);
result.errors.push(msg);
return result;
}
print_success(&format!("Repository cloned to {}", config.repo_dir));
}
if !config.skip_build {
print_step("Building Rusty-Stack TUI...");
let rusty_stack_dir = repo_path.join("rusty-stack");
if !rusty_stack_dir.exists() {
let msg = format!(
"Rust project directory not found: {}",
rusty_stack_dir.display()
);
print_error(&msg);
result.errors.push(msg);
return result;
}
match build_rusty_stack(&rusty_stack_dir.to_string_lossy()) {
Ok(()) => print_success("Rusty-Stack TUI built successfully"),
Err(e) => {
let msg = format!("Failed to build Rusty-Stack TUI: {e}");
print_error(&msg);
result.errors.push(msg);
return result;
}
}
} else {
print_step("Skipping build (using existing binary)");
}
let binary_path = repo_path
.join("rusty-stack")
.join("target")
.join("release")
.join("rusty-stack");
if !binary_path.exists() {
let msg = format!("Binary not found at: {}", binary_path.display());
print_error(&msg);
result.errors.push(msg);
return result;
}
print_success("Installation preparation complete!");
result.success = true;
result.binary_path = Some(binary_path.to_string_lossy().to_string());
result
}
pub fn clone_or_update_repo(
repo_url: &str,
repo_dir: &str,
branch: &str,
) -> Result<String, String> {
let repo_path = PathBuf::from(repo_dir);
if repo_path.join(".git").exists() {
update_repo(repo_dir, branch)?;
} else {
clone_repo(repo_url, repo_dir, branch)?;
}
Ok(repo_dir.to_string())
}
fn install_rust_toolchain() -> Result<(), String> {
let output = Command::new("sh")
.arg("-c")
.arg("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
.output()
.map_err(|e| format!("Failed to run rustup installer: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("rustup installer failed: {stderr}"));
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let cargo_env = format!("{home}/.cargo/env");
if PathBuf::from(&cargo_env).exists() {
let cargo_bin = format!("{home}/.cargo/bin");
let current_path = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", format!("{cargo_bin}:{current_path}"));
Ok(())
} else {
Err("Failed to install Rust toolchain — cargo env not found".to_string())
}
}
fn get_rust_version() -> Result<String, String> {
let output = Command::new("rustc")
.arg("--version")
.output()
.map_err(|e| format!("Failed to get rust version: {e}"))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err("rustc --version failed".to_string())
}
}
fn clone_repo(url: &str, dir: &str, branch: &str) -> Result<(), String> {
let output = Command::new("git")
.args(["clone", "-b", branch, url, dir])
.output()
.map_err(|e| format!("Failed to run git clone: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("git clone failed: {stderr}"))
} else {
Ok(())
}
}
fn update_repo(dir: &str, branch: &str) -> Result<(), String> {
let fetch_output = Command::new("git")
.args(["fetch", "--all"])
.current_dir(dir)
.output()
.map_err(|e| format!("Failed to run git fetch: {e}"))?;
if !fetch_output.status.success() {
let stderr = String::from_utf8_lossy(&fetch_output.stderr);
return Err(format!("git fetch failed: {stderr}"));
}
let reset_output = Command::new("git")
.args(["reset", "--hard", &format!("origin/{branch}")])
.current_dir(dir)
.output()
.map_err(|e| format!("Failed to run git reset: {e}"))?;
if !reset_output.status.success() {
let stderr = String::from_utf8_lossy(&reset_output.stderr);
Err(format!("git reset failed: {stderr}"))
} else {
Ok(())
}
}
fn build_rusty_stack(rusty_stack_dir: &str) -> Result<(), String> {
let output = Command::new("cargo")
.args(["build", "--release"])
.current_dir(rusty_stack_dir)
.output()
.map_err(|e| format!("Failed to run cargo build: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(format!("cargo build failed: {stderr}"))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_install_config_default_repo_url() {
let config = InstallConfig::default();
assert!(config.repo_url.contains("github.com"));
assert!(config.repo_url.contains("Stan-s-ML-Stack"));
}
#[test]
fn test_install_config_default_branch() {
assert_eq!("main", "main");
}
#[test]
fn test_install_config_default_no_skip() {
let config = InstallConfig::default();
assert!(!config.skip_build);
}
#[test]
fn test_install_result_success_fields() {
let result = InstallResult::success(
"/tmp/repo".to_string(),
"/tmp/repo/rusty-stack/target/release/rusty-stack".to_string(),
);
assert!(result.success);
assert_eq!(result.repo_dir, "/tmp/repo");
assert!(result.binary_path.is_some());
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_install_result_failure_fields() {
let result = InstallResult::failure("test error".to_string());
assert!(!result.success);
assert!(result.binary_path.is_none());
assert_eq!(result.errors.len(), 1);
assert!(result.errors[0].contains("test error"));
}
#[test]
fn test_clone_or_update_repo_returns_dir() {
let result: Result<String, String> = clone_or_update_repo(
"https://example.com/repo.git",
"/tmp/nonexistent_test_dir",
"main",
);
assert!(result.is_err() || result.is_ok());
}
#[test]
fn test_get_rust_version_returns_string() {
let _ = get_rust_version();
}
}