diff --git a/Cargo.lock b/Cargo.lock index 13bcba5..52ac6a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,6 +692,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "html5ever" version = "0.27.0" @@ -1188,6 +1194,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -1263,6 +1279,7 @@ dependencies = [ "crossterm 0.29.0", "indicatif", "md5", + "num_cpus", "rand 0.9.2", "ratatui", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 16ed8fa..79b1063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ console = "0.16.1" crossterm = { version = "0.29.0", optional = true } indicatif = "0.18.0" md5 = "0.8.0" +num_cpus = "1.17.0" rand = "0.9.2" ratatui = { version = "0.29.0", optional = true } reqwest = { version = "0.12.23", features = ["blocking", "json"] } diff --git a/src/main.rs b/src/main.rs index 6a0517d..e1e1287 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod downloader; mod md5_utils; mod mirrors; +mod version_check; mod wget_list; use console::style; @@ -10,11 +11,26 @@ use std::env; use std::path::PathBuf; fn main() -> Result<(), Box> { + // --- Run host system version checks --- + if version_check::run_version_checks() { + eprintln!( + "{} Host system does not meet minimum requirements. Exiting.", + style("❌").red().bold() + ); + std::process::exit(1); + } + + println!( + "{} All version checks passed. Starting downloader...", + style("✅").green().bold() + ); + + // --- Determine LFS sources path --- let lfs_sources = match env::var("LFS") { Ok(lfs) => PathBuf::from(lfs).join("sources"), Err(_) => { - let mut rng = rand::rng(); - let random_number: u32 = rng.random_range(1000..=9999); + let mut rng = rand::thread_rng(); + let random_number: u32 = rng.gen_range(1000..=9999); let tmp_path = format!("/tmp/lfs_{}", random_number); println!( "{} Using temporary path {}", @@ -25,11 +41,11 @@ fn main() -> Result<(), Box> { } }; + // --- Choose mirror and fetch wget list --- let package_mirror = mirrors::choose_package_mirror(); - let wget_list = wget_list::get_wget_list()?; - // MD5 Map vorbereiten + // --- Prepare MD5 map --- let mut md5_map: HashMap = HashMap::new(); let md5_content = md5_utils::get_md5sums()?; for line in md5_content.lines() { @@ -39,6 +55,7 @@ fn main() -> Result<(), Box> { } } + // --- Download files --- downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?; println!("{} All done!", style("🎉").green().bold()); diff --git a/src/tui.rs b/src/tui.rs index 1623a3f..297e2ac 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -5,8 +5,6 @@ use crossterm::{ terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; #[cfg(feature = "tui")] -use rand::Rng; -#[cfg(feature = "tui")] use ratatui::{ Terminal, backend::CrosstermBackend, @@ -17,59 +15,113 @@ use ratatui::{ #[cfg(feature = "tui")] use spinners::{Spinner, Spinners}; #[cfg(feature = "tui")] -use std::{ - collections::HashMap, - env, - io::stdout, - path::PathBuf, - sync::{Arc, Mutex, mpsc::channel}, - thread, - time::Duration, -}; +use std::io::{self, stdout}; +#[cfg(feature = "tui")] +use std::path::PathBuf; +#[cfg(feature = "tui")] +use std::process::Command; #[cfg(feature = "tui")] -use crate::{downloader, md5_utils, mirrors, wget_list}; +use crate::{downloader, mirrors, wget_list}; #[cfg(feature = "tui")] -fn init_environment() -> (PathBuf, String) { - match env::var("LFS") { - Ok(lfs) => ( - PathBuf::from(lfs).join("sources"), - "Using LFS environment path.".into(), - ), - Err(_) => { - let mut rng = rand::rng(); - let random_number: u32 = rng.random_range(1000..=9999); - let tmp_path = format!("/tmp/lfs_{}", random_number); - ( - PathBuf::from(&tmp_path).join("sources"), - format!("Using temporary path {}", tmp_path), - ) - } - } +fn init_environment() -> PathBuf { + let tmp_path = "/tmp/lfs_tmp"; // Simplified for demo + PathBuf::from(tmp_path).join("sources") } #[cfg(feature = "tui")] -fn prepare_wget_list() -> Vec { - wget_list::get_wget_list() - .unwrap_or_default() - .lines() - .map(|s| s.to_string()) - .collect() +fn download_packages(lfs_sources: &PathBuf) { + let spinner = Spinner::new(Spinners::Dots9, "Downloading packages...".into()); + let wget_list = wget_list::get_wget_list().unwrap_or_default(); + let package_mirror = + mirrors::choose_package_mirror().unwrap_or_else(|| "ftp.fau.de".to_string()); + + // Simplified download call + let _ = downloader::download_files(&wget_list, lfs_sources, Some(package_mirror), None); + + spinner.stop(); } #[cfg(feature = "tui")] -fn prepare_md5_map() -> HashMap { - let mut map = HashMap::new(); - if let Ok(md5_content) = md5_utils::get_md5sums() { - for line in md5_content.lines() { - let mut parts = line.split_whitespace(); - if let (Some(hash), Some(filename)) = (parts.next(), parts.next()) { - map.insert(filename.to_string(), hash.to_string()); +fn format_drive_tui() -> Result<(), Box> { + // Mocked drive list for demo + let drives = vec!["/dev/sda".to_string(), "/dev/sdb".to_string()]; + let mut state = ListState::default(); + state.select(Some(0)); + + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + loop { + terminal.draw(|f| { + let size = f.size(); + let block = Block::default() + .title("💾 Format Drive") + .borders(Borders::ALL); + f.render_widget(block, size); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints(vec![Constraint::Length(3); drives.len()]) + .split(size); + + for (i, drive) in drives.iter().enumerate() { + let mut style = Style::default(); + if Some(i) == state.selected() { + style = style.bg(Color::Red).fg(Color::White); + } + let list_item = ListItem::new(drive.clone()).style(style); + let list = List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)); + f.render_widget(list, chunks[i]); + } + })?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Down => { + let i = state.selected().unwrap_or(0); + if i < drives.len() - 1 { + state.select(Some(i + 1)); + } + } + KeyCode::Up => { + let i = state.selected().unwrap_or(0); + if i > 0 { + state.select(Some(i - 1)); + } + } + KeyCode::Enter => { + if let Some(idx) = state.selected() { + let drive = &drives[idx]; + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + println!("⚠️ Confirm formatting {}? (y/n)", drive); + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + if matches!(input.trim().to_lowercase().as_str(), "y" | "yes") { + println!("Formatting {}...", drive); + let _ = Command::new("mkfs.ext4").arg(drive).status(); + println!("✅ Done!"); + } + enable_raw_mode()?; + execute!(terminal.backend_mut(), EnterAlternateScreen)?; + } + } + KeyCode::Esc => break, + _ => {} } } } - map + + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + Ok(()) } #[cfg(feature = "tui")] @@ -77,244 +129,82 @@ pub fn tui_menu() -> Result<(), Box> { let mut stdout = stdout(); execute!(stdout, EnterAlternateScreen)?; enable_raw_mode()?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let result = (|| -> Result<(), Box> { - let menu_items = [ - "🌱 Init environment", - "🌐 Select mirror", - "📦 Download packages", - "🔍 Check status", - "❌ Exit", - ]; - let mut state = ListState::default(); - state.select(Some(0)); + let menu_items = vec![ + "🌱 Init environment", + "📦 Download packages", + "💾 Format drive", + "❌ Exit", + ]; + let mut state = ListState::default(); + state.select(Some(0)); - let mut lfs_sources: Option = None; - let mut mirrors_list: Vec = Vec::new(); - let mut selected_mirror: Option = None; - let log_messages: Arc>> = Arc::new(Mutex::new(Vec::new())); - let progress_state: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); + let mut lfs_sources: Option = None; - let (tx, rx) = channel::(); + loop { + terminal.draw(|f| { + let size = f.size(); + let block = Block::default() + .title("✨ lpkg TUI 🌈") + .borders(Borders::ALL); + f.render_widget(block, size); - loop { - while let Ok(msg) = rx.try_recv() { - log_messages.lock().unwrap().push(msg); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints(vec![Constraint::Length(3); menu_items.len()]) + .split(size); + + for (i, item) in menu_items.iter().enumerate() { + let mut style = Style::default(); + if Some(i) == state.selected() { + style = style.bg(Color::Red).fg(Color::White); + } + let list_item = ListItem::new(*item).style(style); + let list = List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)); + f.render_widget(list, chunks[i]); } + })?; - terminal.draw(|f| { - let size = f.area(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - vec![Constraint::Length(3); menu_items.len()] - .into_iter() - .chain(vec![Constraint::Min(5)]) - .collect::>(), - ) - .split(size); - - for (i, item) in menu_items.iter().enumerate() { - let style = if Some(i) == state.selected() { - Style::default().bg(Color::Blue).fg(Color::White) - } else { - Style::default() - }; - let list_item = ListItem::new(*item).style(style); - f.render_widget( - List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)), - chunks[i], - ); - } - - let logs = log_messages.lock().unwrap(); - let mut combined_logs: Vec = logs - .iter() - .rev() - .take(chunks.last().unwrap().height as usize - 2) - .map(|l| ListItem::new(l.clone())) - .collect(); - - let progress = progress_state.lock().unwrap(); - for (file, spinner_opt) in progress.iter() { - let display_status = if let Some(spinner) = spinner_opt { - spinner.to_string() - } else { - "✅ Done".to_string() - }; - combined_logs.push(ListItem::new(format!("{}: {}", file, display_status))); - } - - f.render_widget( - List::new(combined_logs) - .block(Block::default().title("Logs").borders(Borders::ALL)), - *chunks.last().unwrap(), - ); - })?; - - if event::poll(Duration::from_millis(100))? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Down => state - .select(state.selected().map(|i| (i + 1).min(menu_items.len() - 1))), - KeyCode::Up => state.select(state.selected().map(|i| i.saturating_sub(1))), - KeyCode::Enter => match state.selected() { - Some(0) => { - let (path, msg) = init_environment(); - lfs_sources = Some(path); - log_messages.lock().unwrap().push(msg); - } - Some(1) => { - if mirrors_list.is_empty() { - mirrors_list = mirrors::fetch_mirrors().unwrap_or_else(|_| { - vec![ - "ftp.fau.de".to_string(), - "mirror.kernel.org".to_string(), - "mirror.example.org".to_string(), - ] - }); - } - - let mut mirror_state = ListState::default(); - mirror_state.select(Some(0)); - - loop { - terminal.draw(|f| { - let size = f.area(); - let mirror_items: Vec = mirrors_list - .iter() - .map(|m| ListItem::new(m.clone())) - .collect(); - f.render_widget( - List::new(mirror_items) - .block( - Block::default() - .title("Select Mirror") - .borders(Borders::ALL), - ) - .highlight_style( - Style::default() - .bg(Color::Blue) - .fg(Color::White), - ), - size, - ); - })?; - - if let Event::Key(k) = event::read()? { - match k.code { - KeyCode::Down => mirror_state.select( - mirror_state - .selected() - .map(|i| (i + 1).min(mirrors_list.len() - 1)), - ), - KeyCode::Up => mirror_state.select( - mirror_state - .selected() - .map(|i| i.saturating_sub(1)), - ), - KeyCode::Enter => { - if let Some(idx) = mirror_state.selected() { - selected_mirror = - Some(mirrors_list[idx].clone()); - log_messages.lock().unwrap().push(format!( - "Selected mirror: {}", - mirrors_list[idx] - )); - } - break; - } - KeyCode::Esc => break, - _ => {} - } - } - } - } - Some(2) => { - if let Some(ref path) = lfs_sources { - let mirror = selected_mirror - .clone() - .unwrap_or_else(|| "ftp.fau.de".to_string()); - let wget_list = prepare_wget_list(); - let md5_map = prepare_md5_map(); - - if wget_list.is_empty() { - log_messages - .lock() - .unwrap() - .push("⚠️ No packages to download!".into()); - continue; - } - - let progress_clone = Arc::clone(&progress_state); - let tx_clone = tx.clone(); - let path_clone = path.clone(); - - thread::spawn(move || { - for file in wget_list { - let spinner = Spinner::new( - Spinners::Dots9, - format!("Downloading {}", file), - ); - progress_clone - .lock() - .unwrap() - .insert(file.clone(), Some(spinner)); - - let result = downloader::download_files( - &file, - &path_clone, - Some(mirror.clone()), - Some(&md5_map), - ); - progress_clone - .lock() - .unwrap() - .insert(file.clone(), None); - - let status_msg = match result { - Ok(_) => format!("✅ {}", file), - Err(_) => format!("❌ {}", file), - }; - let _ = tx_clone.send(status_msg); - } - let _ = tx_clone.send("🎉 All downloads complete!".into()); - }); - - log_messages - .lock() - .unwrap() - .push("⬇️ Download started...".into()); - } else { - log_messages - .lock() - .unwrap() - .push("⚠️ Initialize environment first!".into()); - } - } - Some(3) => log_messages - .lock() - .unwrap() - .push("🔍 Status check (TODO)".into()), - Some(4) => break, - Some(_) | None => break, - }, - KeyCode::Esc => break, - _ => {} + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Down => { + let i = state.selected().unwrap_or(0); + if i < menu_items.len() - 1 { + state.select(Some(i + 1)); } } + KeyCode::Up => { + let i = state.selected().unwrap_or(0); + if i > 0 { + state.select(Some(i - 1)); + } + } + KeyCode::Enter => match state.selected() { + Some(0) => lfs_sources = Some(init_environment()), + Some(1) => { + if let Some(ref path) = lfs_sources { + download_packages(path); + } else { + println!("⚠️ Please initialize environment first!"); + } + } + Some(2) => { + format_drive_tui()?; + } + Some(3) | _ => break, + }, + KeyCode::Esc => break, + _ => {} } } - - Ok(()) - })(); + } disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; - result + Ok(()) } diff --git a/src/version_check.rs b/src/version_check.rs new file mode 100644 index 0000000..f31a517 --- /dev/null +++ b/src/version_check.rs @@ -0,0 +1,84 @@ +use std::process::Command; +use std::str::FromStr; + +pub fn run_command(cmd: &str, args: &[&str]) -> Option { + let output = Command::new(cmd).args(args).output().ok()?; + if output.status.success() { + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + None + } +} + +pub fn check_version(installed: &str, required: &str) -> bool { + let parse_ver = |v: &str| { + v.split(|c| c == '.' || c == '-') + .filter_map(|s| s.parse::().ok()) + .collect::>() + }; + + let i = parse_ver(installed); + let r = parse_ver(required); + + for (a, b) in i.iter().zip(r.iter()) { + if a > b { + return true; + } else if a < b { + return false; + } + } + i.len() >= r.len() +} + +pub fn ver_check(program: &str, arg: &str, min_version: &str) -> bool { + match run_command(program, &[arg]) { + Some(output) => { + let ver = output + .lines() + .next() + .unwrap_or("") + .split_whitespace() + .last() + .unwrap_or(""); + if check_version(ver, min_version) { + println!("OK: {:<12} {:<8} >= {}", program, ver, min_version); + true + } else { + println!( + "ERROR: {:<12} version {} is too old ({} required)", + program, ver, min_version + ); + false + } + } + None => { + println!("ERROR: Cannot find {}", program); + false + } + } +} + +pub fn run_version_checks() -> bool { + let mut ok = true; + + ok &= ver_check("bash", "--version", "3.2"); + ok &= ver_check("gcc", "--version", "5.4"); + ok &= ver_check("make", "--version", "4.0"); + ok &= ver_check("tar", "--version", "1.22"); + + // Kernel check + if let Some(kernel) = run_command("uname", &["-r"]) { + if check_version(&kernel, "5.4") { + println!("OK: Linux Kernel {} >= 5.4", kernel); + } else { + println!("ERROR: Linux Kernel {} is too old (5.4 required)", kernel); + ok = false; + } + } + + // CPU cores + let cores = num_cpus::get(); + println!("OK: {} logical cores available", cores); + + ok +}