rusty_stack/bootstrap/
install.rs1use crate::installers::common::utils::{
12 command_exists, print_error, print_step, print_success, print_warning,
13};
14use std::path::PathBuf;
15use std::process::Command;
16
17#[derive(Debug, Clone)]
25pub struct InstallConfig {
26 pub repo_dir: String,
29 pub repo_url: String,
32 pub skip_build: bool,
34 pub branch: String,
37}
38
39impl Default for InstallConfig {
40 fn default() -> Self {
41 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
42 Self {
43 repo_dir: std::env::var("REPO_DIR")
44 .unwrap_or_else(|_| format!("{home}/Stan-s-ML-Stack")),
45 repo_url: "https://github.com/scooter-lacroix/Stan-s-ML-Stack.git".to_string(),
46 skip_build: false,
47 branch: std::env::var("BRANCH").unwrap_or_else(|_| "main".to_string()),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct InstallResult {
55 pub success: bool,
57 pub binary_path: Option<String>,
59 pub repo_dir: String,
61 pub errors: Vec<String>,
63 pub warnings: Vec<String>,
65}
66
67impl InstallResult {
68 pub fn success(repo_dir: String, binary_path: String) -> Self {
70 Self {
71 success: true,
72 binary_path: Some(binary_path),
73 repo_dir,
74 errors: Vec::new(),
75 warnings: Vec::new(),
76 }
77 }
78
79 pub fn failure(error: String) -> Self {
81 Self {
82 success: false,
83 binary_path: None,
84 repo_dir: String::new(),
85 errors: vec![error],
86 warnings: Vec::new(),
87 }
88 }
89}
90
91pub fn install_sh(config: &InstallConfig) -> InstallResult {
101 let mut result = InstallResult {
102 success: false,
103 binary_path: None,
104 repo_dir: config.repo_dir.clone(),
105 errors: Vec::new(),
106 warnings: Vec::new(),
107 };
108
109 print_step("Checking dependencies...");
111
112 if !command_exists("git") {
113 let msg = "git is not installed. Please install git first.".to_string();
114 print_error(&msg);
115 result.errors.push(msg);
116 return result;
117 }
118 print_success("git found");
119
120 if !command_exists("rustc") || !command_exists("cargo") {
122 print_warning("Rust toolchain not found.");
123 print_step("Attempting to install Rust via rustup...");
124
125 if !command_exists("curl") {
126 let msg = "curl is required to install Rust. Please install curl first.".to_string();
127 print_error(&msg);
128 result.errors.push(msg);
129 return result;
130 }
131
132 match install_rust_toolchain() {
134 Ok(()) => print_success("Rust toolchain installed"),
135 Err(e) => {
136 let msg = format!("Failed to install Rust toolchain: {e}");
137 print_error(&msg);
138 result.errors.push(msg);
139 return result;
140 }
141 }
142 } else if let Ok(version) = get_rust_version() {
143 print_success(&format!("Rust toolchain found: {version}"));
144 }
145
146 let repo_path = PathBuf::from(&config.repo_dir);
148 if repo_path.join(".git").exists() {
149 print_step(&format!(
150 "Repository exists at {}. Updating...",
151 config.repo_dir
152 ));
153 if let Err(e) = update_repo(&config.repo_dir, &config.branch) {
154 result
155 .warnings
156 .push(format!("Failed to update repository: {e}"));
157 print_warning(&format!("Repository update failed: {e}"));
158 } else {
159 print_success("Repository updated");
160 }
161 } else {
162 print_step(&format!("Cloning repository from {}...", config.repo_url));
163 if let Err(e) = clone_repo(&config.repo_url, &config.repo_dir, &config.branch) {
164 let msg = format!("Failed to clone repository: {e}");
165 print_error(&msg);
166 result.errors.push(msg);
167 return result;
168 }
169 print_success(&format!("Repository cloned to {}", config.repo_dir));
170 }
171
172 if !config.skip_build {
174 print_step("Building Rusty-Stack TUI...");
175 let rusty_stack_dir = repo_path.join("rusty-stack");
176 if !rusty_stack_dir.exists() {
177 let msg = format!(
178 "Rust project directory not found: {}",
179 rusty_stack_dir.display()
180 );
181 print_error(&msg);
182 result.errors.push(msg);
183 return result;
184 }
185
186 match build_rusty_stack(&rusty_stack_dir.to_string_lossy()) {
187 Ok(()) => print_success("Rusty-Stack TUI built successfully"),
188 Err(e) => {
189 let msg = format!("Failed to build Rusty-Stack TUI: {e}");
190 print_error(&msg);
191 result.errors.push(msg);
192 return result;
193 }
194 }
195 } else {
196 print_step("Skipping build (using existing binary)");
197 }
198
199 let binary_path = repo_path
201 .join("rusty-stack")
202 .join("target")
203 .join("release")
204 .join("rusty-stack");
205
206 if !binary_path.exists() {
207 let msg = format!("Binary not found at: {}", binary_path.display());
208 print_error(&msg);
209 result.errors.push(msg);
210 return result;
211 }
212
213 print_success("Installation preparation complete!");
214 result.success = true;
215 result.binary_path = Some(binary_path.to_string_lossy().to_string());
216 result
217}
218
219pub fn clone_or_update_repo(
223 repo_url: &str,
224 repo_dir: &str,
225 branch: &str,
226) -> Result<String, String> {
227 let repo_path = PathBuf::from(repo_dir);
228 if repo_path.join(".git").exists() {
229 update_repo(repo_dir, branch)?;
230 } else {
231 clone_repo(repo_url, repo_dir, branch)?;
232 }
233 Ok(repo_dir.to_string())
234}
235
236fn install_rust_toolchain() -> Result<(), String> {
242 let output = Command::new("sh")
243 .arg("-c")
244 .arg("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
245 .output()
246 .map_err(|e| format!("Failed to run rustup installer: {e}"))?;
247
248 if !output.status.success() {
249 let stderr = String::from_utf8_lossy(&output.stderr);
250 return Err(format!("rustup installer failed: {stderr}"));
251 }
252
253 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
255 let cargo_env = format!("{home}/.cargo/env");
256 if PathBuf::from(&cargo_env).exists() {
257 let cargo_bin = format!("{home}/.cargo/bin");
259 let current_path = std::env::var("PATH").unwrap_or_default();
260 std::env::set_var("PATH", format!("{cargo_bin}:{current_path}"));
261 Ok(())
262 } else {
263 Err("Failed to install Rust toolchain — cargo env not found".to_string())
264 }
265}
266
267fn get_rust_version() -> Result<String, String> {
269 let output = Command::new("rustc")
270 .arg("--version")
271 .output()
272 .map_err(|e| format!("Failed to get rust version: {e}"))?;
273
274 if output.status.success() {
275 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
276 } else {
277 Err("rustc --version failed".to_string())
278 }
279}
280
281fn clone_repo(url: &str, dir: &str, branch: &str) -> Result<(), String> {
283 let output = Command::new("git")
284 .args(["clone", "-b", branch, url, dir])
285 .output()
286 .map_err(|e| format!("Failed to run git clone: {e}"))?;
287
288 if !output.status.success() {
289 let stderr = String::from_utf8_lossy(&output.stderr);
290 Err(format!("git clone failed: {stderr}"))
291 } else {
292 Ok(())
293 }
294}
295
296fn update_repo(dir: &str, branch: &str) -> Result<(), String> {
298 let fetch_output = Command::new("git")
299 .args(["fetch", "--all"])
300 .current_dir(dir)
301 .output()
302 .map_err(|e| format!("Failed to run git fetch: {e}"))?;
303
304 if !fetch_output.status.success() {
305 let stderr = String::from_utf8_lossy(&fetch_output.stderr);
306 return Err(format!("git fetch failed: {stderr}"));
307 }
308
309 let reset_output = Command::new("git")
310 .args(["reset", "--hard", &format!("origin/{branch}")])
311 .current_dir(dir)
312 .output()
313 .map_err(|e| format!("Failed to run git reset: {e}"))?;
314
315 if !reset_output.status.success() {
316 let stderr = String::from_utf8_lossy(&reset_output.stderr);
317 Err(format!("git reset failed: {stderr}"))
318 } else {
319 Ok(())
320 }
321}
322
323fn build_rusty_stack(rusty_stack_dir: &str) -> Result<(), String> {
325 let output = Command::new("cargo")
326 .args(["build", "--release"])
327 .current_dir(rusty_stack_dir)
328 .output()
329 .map_err(|e| format!("Failed to run cargo build: {e}"))?;
330
331 if !output.status.success() {
332 let stderr = String::from_utf8_lossy(&output.stderr);
333 Err(format!("cargo build failed: {stderr}"))
334 } else {
335 Ok(())
336 }
337}
338
339#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_install_config_default_repo_url() {
349 let config = InstallConfig::default();
350 assert!(config.repo_url.contains("github.com"));
351 assert!(config.repo_url.contains("Stan-s-ML-Stack"));
352 }
353
354 #[test]
355 fn test_install_config_default_branch() {
356 assert_eq!("main", "main");
359 }
360
361 #[test]
362 fn test_install_config_default_no_skip() {
363 let config = InstallConfig::default();
365 assert!(!config.skip_build);
366 }
367
368 #[test]
369 fn test_install_result_success_fields() {
370 let result = InstallResult::success(
371 "/tmp/repo".to_string(),
372 "/tmp/repo/rusty-stack/target/release/rusty-stack".to_string(),
373 );
374 assert!(result.success);
375 assert_eq!(result.repo_dir, "/tmp/repo");
376 assert!(result.binary_path.is_some());
377 assert!(result.errors.is_empty());
378 assert!(result.warnings.is_empty());
379 }
380
381 #[test]
382 fn test_install_result_failure_fields() {
383 let result = InstallResult::failure("test error".to_string());
384 assert!(!result.success);
385 assert!(result.binary_path.is_none());
386 assert_eq!(result.errors.len(), 1);
387 assert!(result.errors[0].contains("test error"));
388 }
389
390 #[test]
391 fn test_clone_or_update_repo_returns_dir() {
392 let result: Result<String, String> = clone_or_update_repo(
395 "https://example.com/repo.git",
396 "/tmp/nonexistent_test_dir",
397 "main",
398 );
399 assert!(result.is_err() || result.is_ok());
401 }
402
403 #[test]
404 fn test_get_rust_version_returns_string() {
405 let _ = get_rust_version();
407 }
408}