This commit is contained in:
Lucy 2025-09-30 18:59:48 +02:00
parent db62ec1d88
commit 85b4ad55b3
5 changed files with 279 additions and 270 deletions

17
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

View file

@ -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<dyn std::error::Error>> {
// --- 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<dyn std::error::Error>> {
}
};
// --- 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<String, String> = HashMap::new();
let md5_content = md5_utils::get_md5sums()?;
for line in md5_content.lines() {
@ -39,6 +55,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// --- Download files ---
downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?;
println!("{} All done!", style("🎉").green().bold());

View file

@ -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<String> {
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<String, String> {
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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<PathBuf> = None;
let mut mirrors_list: Vec<String> = Vec::new();
let mut selected_mirror: Option<String> = None;
let log_messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let progress_state: Arc<Mutex<HashMap<String, Option<Spinner>>>> =
Arc::new(Mutex::new(HashMap::new()));
let mut lfs_sources: Option<PathBuf> = None;
let (tx, rx) = channel::<String>();
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::<Vec<_>>(),
)
.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<ListItem> = 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<ListItem> = 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(())
}

84
src/version_check.rs Normal file
View file

@ -0,0 +1,84 @@
use std::process::Command;
use std::str::FromStr;
pub fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
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::<u32>().ok())
.collect::<Vec<_>>()
};
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
}