This commit is contained in:
Lucy 2025-09-30 20:38:13 +02:00
parent 23905877c7
commit 208234df27
7 changed files with 478 additions and 127 deletions

217
Cargo.lock generated
View file

@ -30,6 +30,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@ -131,6 +140,15 @@ version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.19.0"
@ -278,6 +296,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crossterm"
version = "0.28.1"
@ -321,6 +348,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "cssparser"
version = "0.31.2"
@ -411,6 +448,16 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -422,6 +469,12 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "document-features"
version = "0.2.11"
@ -606,6 +659,16 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.24"
@ -712,6 +775,21 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "html_parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f56db07b6612644f6f7719f8ef944f75fff9d6378fdf3d316fd32194184abd"
dependencies = [
"doc-comment",
"pest",
"pest_derive",
"serde",
"serde_derive",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1277,14 +1355,17 @@ dependencies = [
"clap",
"console",
"crossterm 0.29.0",
"html_parser",
"indicatif",
"md5",
"num_cpus",
"rand 0.9.2",
"ratatui",
"regex",
"reqwest",
"scraper",
"serde",
"serde_json",
"spinners",
]
@ -1323,6 +1404,50 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
dependencies = [
"memchr",
"thiserror 2.0.17",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "pest_meta"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
dependencies = [
"pest",
"sha2",
]
[[package]]
name = "phf"
version = "0.10.1"
@ -1574,6 +1699,35 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "reqwest"
version = "0.12.23"
@ -1844,6 +1998,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -2105,6 +2270,46 @@ dependencies = [
"utf-8",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl 2.0.17",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "tinystr"
version = "0.8.1"
@ -2235,6 +2440,18 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-ident"
version = "1.0.19"

View file

@ -8,15 +8,18 @@ anyhow = "1.0.100"
clap = { version = "4.5.48", features= ["derive"] }
console = "0.16.1"
crossterm = { version = "0.29.0", optional = true }
html_parser = "0.7.0"
indicatif = "0.18.0"
md5 = "0.8.0"
num_cpus = "1.17.0"
rand = "0.9.2"
ratatui = { version = "0.29.0", optional = true }
regex = "1.11.3"
reqwest = { version = "0.12.23", features = ["blocking", "json"] }
scraper = "0.19.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
spinners = "4.1.1"
[features]

33
src/html.rs Normal file
View file

@ -0,0 +1,33 @@
use html_parser::Dom;
use reqwest::blocking::get;
use std::error::Error;
/// Lädt die HTML-Seite von der angegebenen URL herunter und konvertiert sie in JSON
pub fn fetch_and_parse_html_to_json(url: &str) -> Result<String, Box<dyn Error>> {
// HTML herunterladen
let response = get(url)?;
if !response.status().is_success() {
return Err(format!("Fehler beim Abrufen der URL {}: {}", url, response.status()).into());
}
let body = response.text()?;
// HTML parsen
let dom = Dom::parse(&body)?;
// In JSON konvertieren
let json = dom.to_json_pretty()?;
Ok(json)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fetch_and_parse() {
let url = "https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html";
let json = fetch_and_parse_html_to_json(url).expect("Fehler beim Parsen");
assert!(json.contains("Host System Requirements"));
}
}

View file

@ -1,11 +1,12 @@
mod downloader;
mod html;
mod md5_utils;
mod mirrors;
mod version_check;
mod wget_list;
#[cfg(feature = "tui")]
mod tui; // Importiere das TUI-Modul, wenn das Feature aktiv ist
mod tui;
use console::style;
use rand::Rng;
@ -16,34 +17,30 @@ use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tui")]
{
// Wenn das TUI-Feature aktiv ist, starte das TUI-Menü
// TUI-Modus
tui::tui_menu()?;
Ok(())
}
#[cfg(not(feature = "tui"))]
{
// Wenn das TUI-Feature NICHT aktiv ist, führe die CLI-Logik aus
// --- 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);
}
// --- Dynamische Version-Prüfung aus HTML ---
let json = html::fetch_and_parse_html_to_json(
"https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html",
)?;
version_check::run_version_checks_from_json(&json);
println!(
"{} All version checks passed. Starting downloader...",
style("").green().bold()
);
// --- Determine LFS sources path ---
// --- Bestimme LFS-Sources-Pfad ---
let lfs_sources = match env::var("LFS") {
Ok(lfs) => PathBuf::from(lfs).join("sources"),
Err(_) => {
let mut rng = rand::thread_rng(); // Verwende thread_rng() statt rng()
let random_number: u32 = rng.gen_range(1000..=9999); // Verwende gen_range() statt random_range()
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 {}",
@ -54,20 +51,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
};
// --- Choose mirror and fetch wget list ---
// Diese Zeile wird entfernt, da die Mirror-Auswahl in der TUI erfolgt
// let package_mirror = mirrors::choose_package_mirror();
// Da die Mirror-Auswahl nun in der TUI erfolgt, müssen wir hier einen Standardwert oder eine andere Logik verwenden,
// wenn das TUI nicht aktiv ist. Für dieses Beispiel nehmen wir an, dass wir keinen Mirror verwenden,
// wenn das TUI nicht aktiv ist, oder wir könnten eine andere CLI-basierte Auswahl implementieren.
// Für den Moment setzen wir es auf None, was bedeutet, dass der Standard-Mirror verwendet wird.
// --- CLI Mirror-Auswahl: default oder erweiterbar ---
let package_mirror: Option<String> = None;
// --- Hole wget-Liste ---
let wget_list = wget_list::get_wget_list()?;
// --- Prepare MD5 map ---
// --- Bereite MD5-Map vor ---
let mut md5_map: HashMap<String, String> = HashMap::new();
let md5_content = md5_utils::get_md5sums()?;
for line in md5_content.lines() {
@ -77,7 +67,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// --- Download files ---
// --- Lade Dateien herunter ---
downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?;
println!("{} All done!", style("🎉").green().bold());

View file

@ -3,6 +3,29 @@ use reqwest::blocking::Client;
use scraper::{Html, Selector};
use std::io::{self, Write};
pub fn fetch_mirrors() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let client = Client::new();
let res = client
.get("https://www.linuxfromscratch.org/lfs/mirrors.html#files")
.send()?
.text()?;
let document = Html::parse_document(&res);
let selector = Selector::parse("a[href^='http']").unwrap();
let mirrors = document
.select(&selector)
.filter_map(|element| {
let href = element.value().attr("href")?;
// Basic filtering to get potential mirror URLs
if href.contains("ftp.gnu.org") || href.contains("mirror") {
Some(href.to_string())
} else {
None
}
})
.collect();
Ok(mirrors)
}
pub fn choose_package_mirror() -> Option<String> {
let mirrors = match fetch_mirrors() {
Ok(mirrors) => mirrors,
@ -42,26 +65,3 @@ pub fn choose_package_mirror() -> Option<String> {
Some(chosen.to_string())
}
}
pub fn fetch_mirrors() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let client = Client::new();
let res = client
.get("https://www.linuxfromscratch.org/lfs/mirrors.html#files")
.send()?
.text()?;
let document = Html::parse_document(&res);
let selector = Selector::parse("a[href^='http']").unwrap();
let mirrors = document
.select(&selector)
.filter_map(|element| {
let href = element.value().attr("href")?;
// Basic filtering to get potential mirror URLs
if href.contains("ftp.gnu.org") || href.contains("mirror") {
Some(href.to_string())
} else {
None
}
})
.collect();
Ok(mirrors)
}

View file

@ -1,4 +1,6 @@
#[cfg(feature = "tui")]
use crate::html::fetch_and_parse_html_to_json;
#[cfg(feature = "tui")]
use crate::{downloader, md5_utils, mirrors, wget_list};
#[cfg(feature = "tui")]
use crossterm::{
@ -7,6 +9,8 @@ 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,25 +21,47 @@ use ratatui::{
#[cfg(feature = "tui")]
use std::{
collections::HashMap,
fs,
io::stdout,
path::PathBuf,
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
fs, // Added for file system operations
};
#[cfg(feature = "tui")] // Hinzugefügt: Import des Rng-Traits
use rand::Rng;
#[cfg(feature = "tui")]
use reqwest::blocking::get;
#[cfg(feature = "tui")]
use scraper::{Html, Selector};
#[cfg(feature = "tui")]
use serde_json::json;
// ----------------- HTML fetch function -----------------
#[cfg(feature = "tui")]
fn fetch_html_to_json(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let body = get(url)?.text()?;
let document = Html::parse_document(&body);
let selector = Selector::parse("body").unwrap();
let mut contents = vec![];
for element in document.select(&selector) {
contents.push(element.text().collect::<Vec<_>>().join(" "));
}
Ok(serde_json::to_string_pretty(
&json!({ "body_text": contents }),
)?)
}
// ----------------- TUI functions -----------------
#[cfg(feature = "tui")]
fn init_environment() -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut rng = rand::rng();
let mut rng = rand::thread_rng();
let random_number: u32 = rng.gen_range(1000..=9999);
let tmp_base_path = PathBuf::from(format!("/tmp/lfs_{}", random_number));
let lfs_sources_path = tmp_base_path.join("sources");
std::fs::create_dir_all(&lfs_sources_path)?;
fs::create_dir_all(&lfs_sources_path)?;
Ok(lfs_sources_path)
}
@ -58,7 +84,7 @@ fn select_mirrors_tui(mirrors: Vec<String>) -> Vec<String> {
loop {
terminal
.draw(|f| {
let size = f.area();
let size = f.size();
let items: Vec<ListItem> = mirrors
.iter()
.enumerate()
@ -70,8 +96,8 @@ fn select_mirrors_tui(mirrors: Vec<String>) -> Vec<String> {
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title("Select mirrors"),
.title("Select mirrors")
.borders(Borders::ALL),
)
.highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
@ -182,7 +208,7 @@ fn download_packages_tui(lfs_sources: &PathBuf) {
loop {
terminal
.draw(|f| {
let size = f.area();
let size = f.size();
let items: Vec<ListItem> = {
let state = download_state.lock().unwrap();
state
@ -210,6 +236,26 @@ fn download_packages_tui(lfs_sources: &PathBuf) {
execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap();
}
// ----------------- Cleanup function -----------------
#[cfg(feature = "tui")]
fn cleanup_temp_directories() -> Result<usize, Box<dyn std::error::Error>> {
let mut cleaned_count = 0;
for entry in fs::read_dir("/tmp")? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
if name.starts_with("lfs_") {
fs::remove_dir_all(&path)?;
cleaned_count += 1;
}
}
}
}
Ok(cleaned_count)
}
// ----------------- Main TUI menu -----------------
#[cfg(feature = "tui")]
pub fn tui_menu() -> Result<(), Box<dyn std::error::Error>> {
let mut stdout = stdout();
@ -222,19 +268,20 @@ pub fn tui_menu() -> Result<(), Box<dyn std::error::Error>> {
"🌱 Init environment",
"📦 Download packages",
"🔍 Check status",
"🧹 Clean up temp directories", // Neuer Menüpunkt
"🧹 Clean up temp directories",
"📄 Test JSON fetch", // NEW BUTTON
"❌ Exit",
];
let mut state = ListState::default();
state.select(Some(0));
let mut lfs_sources: Option<PathBuf> = None;
let mut status_message: Option<String> = None; // Für Statusmeldungen
let mut status_message_timer: Option<std::time::Instant> = None; // Für temporäre Meldungen
let mut status_message: Option<String> = None;
let mut status_message_timer: Option<Instant> = None;
loop {
terminal.draw(|f| {
let size = f.area();
let size = f.size();
let block = Block::default()
.title("✨ lpkg TUI 🌈")
.borders(Borders::ALL);
@ -256,34 +303,35 @@ pub fn tui_menu() -> Result<(), Box<dyn std::error::Error>> {
f.render_widget(list, chunks[i]);
}
// Statusmeldung anzeigen
if let Some(msg) = &status_message {
let msg_block = Block::default()
let paragraph = Paragraph::new(msg.as_str()).block(
Block::default()
.borders(Borders::NONE)
.style(Style::default().fg(Color::Yellow));
let paragraph = Paragraph::new(msg.as_str()).block(msg_block);
// Positionierung der Statusmeldung (z.B. unten in der Mitte)
.style(Style::default().fg(Color::Yellow)),
);
let msg_area = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(10), Constraint::Percentage(80), Constraint::Percentage(10)])
.split(size)[1]; // Mittlerer Bereich
.constraints([
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
])
.split(size)[1];
let msg_area = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(1)])
.split(msg_area)[1]; // Unterste Zeile
.split(msg_area)[1];
f.render_widget(paragraph, msg_area);
}
})?;
// Statusmeldung nach einer bestimmten Zeit ausblenden
if let Some(timer) = status_message_timer {
if timer.elapsed() > Duration::from_secs(3) { // 3 Sekunden anzeigen
if timer.elapsed() > Duration::from_secs(3) {
status_message = None;
status_message_timer = None;
}
}
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Down => {
@ -300,44 +348,61 @@ pub fn tui_menu() -> Result<(), Box<dyn std::error::Error>> {
}
KeyCode::Enter => match state.selected() {
Some(0) => {
// Init environment
match init_environment() {
Ok(path) => {
lfs_sources = Some(path.clone());
status_message = Some(format!("✅ Environment initialized: {}", path.display()));
status_message =
Some(format!("✅ Environment initialized: {}", path.display()));
}
Err(e) => {
status_message = Some(format!("❌ Failed to initialize environment: {}", e));
status_message =
Some(format!("❌ Failed to init environment: {}", e));
}
}
status_message_timer = Some(std::time::Instant::now());
status_message_timer = Some(Instant::now());
}
Some(1) => {
if let Some(ref path) = lfs_sources {
// Download packages
if let Some(path) = &lfs_sources {
download_packages_tui(path);
} else {
status_message = Some("⚠️ Please initialize environment first!".to_string());
status_message_timer = Some(std::time::Instant::now());
}
}
Some(2) => {
// Status
status_message = Some("🔍 Status selected! (TODO)".to_string());
status_message_timer = Some(std::time::Instant::now());
status_message_timer = Some(Instant::now());
}
Some(3) => {
// Aufruf der neuen Bereinigungsfunktion
status_message = Some("🧹 Cleaning up temporary directories...".to_string());
status_message_timer = Some(std::time::Instant::now());
// Cleanup
match cleanup_temp_directories() {
Ok(count) => {
status_message = Some(format!("✅ Cleaned up {} temporary directories.", count));
status_message =
Some(format!("✅ Cleaned {} temporary dirs", count));
}
Err(e) => {
status_message = Some(format!("❌ Failed to clean up: {}", e));
status_message = Some(format!("❌ Failed cleanup: {}", e));
}
}
status_message_timer = Some(std::time::Instant::now());
status_message_timer = Some(Instant::now());
}
Some(4) | _ => break,
Some(4) => {
// Test JSON fetch
// Leave TUI first
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
match fetch_and_parse_html_to_json(
"https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html",
) {
Ok(json) => println!("✅ JSON output:\n{}", json),
Err(e) => eprintln!("❌ Error: {}", e),
}
return Ok(());
}
Some(5) | _ => break,
},
KeyCode::Esc => break,
_ => {}
@ -350,21 +415,3 @@ pub fn tui_menu() -> Result<(), Box<dyn std::error::Error>> {
terminal.show_cursor()?;
Ok(())
}
#[cfg(feature = "tui")]
fn cleanup_temp_directories() -> Result<usize, Box<dyn std::error::Error>> {
let mut cleaned_count = 0;
for entry in std::fs::read_dir("/tmp")? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) {
if dir_name.starts_with("lfs_") {
std::fs::remove_dir_all(&path)?;
cleaned_count += 1;
}
}
}
}
Ok(cleaned_count)
}

View file

@ -1,6 +1,9 @@
use reqwest;
use scraper::{Html, Selector};
use std::process::Command;
pub fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
/// Führt ein Kommando aus und gibt die erste Zeile der Version zurück
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())
@ -9,7 +12,8 @@ pub fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
}
}
pub fn check_version(installed: &str, required: &str) -> bool {
/// Vergleicht zwei Versionen
fn check_version(installed: &str, required: &str) -> bool {
let parse_ver = |v: &str| {
v.split(['.', '-'])
.filter_map(|s| s.parse::<u32>().ok())
@ -29,8 +33,9 @@ pub fn check_version(installed: &str, required: &str) -> bool {
i.len() >= r.len()
}
pub fn ver_check(program: &str, arg: &str, min_version: &str) -> bool {
match run_command(program, &[arg]) {
/// Führt eine Version-Prüfung durch
fn ver_check(program: &str, cmd: &str, min_version: &str) -> bool {
match run_command(cmd, &["--version"]) {
Some(output) => {
let ver = output
.lines()
@ -57,27 +62,83 @@ pub fn ver_check(program: &str, arg: &str, min_version: &str) -> bool {
}
}
pub fn run_version_checks() -> bool {
/// Führt die Kernel-Prüfung durch
fn ver_kernel(min_version: &str) -> bool {
let kernel = run_command("uname", &["-r"]).unwrap_or_default();
if check_version(&kernel, min_version) {
println!("OK: Linux Kernel {} >= {}", kernel, min_version);
true
} else {
println!(
"ERROR: Linux Kernel {} is too old ({} required)",
kernel, min_version
);
false
}
}
/// Lädt die LFS-Seite und führt alle Versionsprüfungen aus
pub fn run_version_checks_from_html(url: &str) -> Result<(), Box<dyn std::error::Error>> {
let html_text = reqwest::blocking::get(url)?.text()?;
let document = Html::parse_document(&html_text);
let selector = Selector::parse("pre").unwrap();
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");
for element in document.select(&selector) {
let pre_text = element.text().collect::<Vec<_>>().join("\n");
// Kernel check
if let Some(kernel) = run_command("uname", &["-r"]) {
if check_version(&kernel, "5.4") {
println!("OK: Linux Kernel {} >= 5.4", kernel);
for line in pre_text.lines() {
let line = line.trim();
if line.starts_with("ver_check") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let prog = parts[1];
let cmd = parts[2];
let ver = parts[3];
ok &= ver_check(prog, cmd, ver);
}
} else if line.starts_with("ver_kernel") {
if let Some(ver) = line.split_whitespace().nth(1) {
ok &= ver_kernel(ver);
}
}
}
}
// Alias-Checks
let alias_check = |cmd: &str, expected: &str| {
if let Some(output) = run_command(cmd, &["--version"]) {
if output.to_lowercase().contains(&expected.to_lowercase()) {
println!("OK: {:<4} is {}", cmd, expected);
} else {
println!("ERROR: Linux Kernel {} is too old (5.4 required)", kernel);
ok = false;
println!("ERROR: {:<4} is NOT {}", cmd, expected);
}
}
};
// CPU cores
let cores = num_cpus::get();
println!("OK: {} logical cores available", cores);
alias_check("awk", "GNU");
alias_check("yacc", "Bison");
alias_check("sh", "Bash");
ok
// Compiler-Test
if run_command("g++", &["-x", "c++", "-"]).is_some() {
println!("OK: g++ works");
} else {
println!("ERROR: g++ does NOT work");
}
// nproc-Test
let nproc = run_command("nproc", &[]).unwrap_or_default();
if nproc.is_empty() {
println!("ERROR: nproc is not available or empty");
} else {
println!("OK: nproc reports {} logical cores available", nproc);
}
if !ok {
println!("Some version checks failed.");
}
Ok(())
}