This commit is contained in:
Lucy 2025-09-30 21:38:22 +02:00
parent 1eb7d4c1eb
commit 35c3f4c423
10 changed files with 353 additions and 738 deletions

186
Cargo.lock generated
View file

@ -134,6 +134,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.4" version = "2.9.4"
@ -305,15 +311,31 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crossterm"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.28.1" version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"crossterm_winapi", "crossterm_winapi",
"mio", "mio 1.0.4",
"parking_lot", "parking_lot",
"rustix 0.38.44", "rustix 0.38.44",
"signal-hook", "signal-hook",
@ -327,11 +349,11 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"crossterm_winapi", "crossterm_winapi",
"derive_more 2.0.1", "derive_more 2.0.1",
"document-features", "document-features",
"mio", "mio 1.0.4",
"parking_lot", "parking_lot",
"rustix 1.1.2", "rustix 1.1.2",
"signal-hook", "signal-hook",
@ -499,6 +521,12 @@ dependencies = [
"dtoa", "dtoa",
] ]
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]] [[package]]
name = "ego-tree" name = "ego-tree"
version = "0.6.3" version = "0.6.3"
@ -650,6 +678,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]] [[package]]
name = "fxhash" name = "fxhash"
version = "0.2.1" version = "0.2.1"
@ -1052,6 +1089,20 @@ version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "inquire"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a"
dependencies = [
"bitflags 2.9.4",
"crossterm 0.29.0",
"dyn-clone",
"fuzzy-matcher",
"unicode-segmentation",
"unicode-width 0.2.0",
]
[[package]] [[package]]
name = "instability" name = "instability"
version = "0.3.9" version = "0.3.9"
@ -1071,7 +1122,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"cfg-if", "cfg-if",
"libc", "libc",
] ]
@ -1237,6 +1288,18 @@ dependencies = [
"adler2", "adler2",
] ]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.4" version = "1.0.4"
@ -1309,7 +1372,7 @@ version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"cfg-if", "cfg-if",
"foreign-types", "foreign-types",
"libc", "libc",
@ -1357,6 +1420,7 @@ dependencies = [
"crossterm 0.29.0", "crossterm 0.29.0",
"html_parser", "html_parser",
"indicatif", "indicatif",
"inquire",
"md5", "md5",
"num_cpus", "num_cpus",
"rand 0.9.2", "rand 0.9.2",
@ -1368,6 +1432,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"spinners", "spinners",
"tui",
] ]
[[package]] [[package]]
@ -1676,7 +1741,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"cassowary", "cassowary",
"compact_str", "compact_str",
"crossterm 0.28.1", "crossterm 0.28.1",
@ -1697,7 +1762,7 @@ version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
] ]
[[package]] [[package]]
@ -1797,7 +1862,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
@ -1810,7 +1875,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.11.0", "linux-raw-sys 0.11.0",
@ -1899,7 +1964,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1922,7 +1987,7 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"cssparser", "cssparser",
"derive_more 0.99.20", "derive_more 0.99.20",
"fxhash", "fxhash",
@ -2039,7 +2104,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [ dependencies = [
"libc", "libc",
"mio", "mio 0.8.11",
"mio 1.0.4",
"signal-hook", "signal-hook",
] ]
@ -2238,7 +2304,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys",
] ]
@ -2317,6 +2383,15 @@ dependencies = [
"syn 2.0.106", "syn 2.0.106",
] ]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@ -2337,7 +2412,7 @@ dependencies = [
"bytes", "bytes",
"io-uring", "io-uring",
"libc", "libc",
"mio", "mio 1.0.4",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
"socket2", "socket2",
@ -2398,7 +2473,7 @@ version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.9.4",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -2447,6 +2522,19 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tui"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags 1.3.2",
"cassowary",
"crossterm 0.25.0",
"unicode-segmentation",
"unicode-width 0.1.14",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"
@ -2736,6 +2824,15 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@ -2772,6 +2869,21 @@ dependencies = [
"windows-link 0.2.0", "windows-link 0.2.0",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@ -2805,6 +2917,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0", "windows_x86_64_msvc 0.53.0",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -2817,6 +2935,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@ -2829,6 +2953,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@ -2853,6 +2983,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@ -2865,6 +3001,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@ -2877,6 +3019,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@ -2889,6 +3037,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"

View file

@ -33,6 +33,8 @@ md5 = "0.8.0"
# HTTP # HTTP
reqwest = { version = "0.12.23", features = ["blocking", "json"] } reqwest = { version = "0.12.23", features = ["blocking", "json"] }
semver = "1.0.27" semver = "1.0.27"
inquire = "0.9.1"
tui = "0.19.0"
[features] [features]
# TUI feature flag # TUI feature flag

View file

@ -1,122 +1,54 @@
use anyhow::{Result, anyhow};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use std::thread; use std::thread;
use console::style;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use reqwest::blocking::Client;
fn verify_md5(file_path: &Path, expected_hash: &str) -> bool {
let mut f = match File::open(file_path) {
Ok(f) => f,
Err(_) => return false,
};
let mut buffer = Vec::new();
if f.read_to_end(&mut buffer).is_err() {
return false;
}
let digest = md5::compute(&buffer);
let hex = format!("{:x}", digest);
hex == expected_hash
}
pub fn download_files( pub fn download_files(
wget_list: &str, files: &[String],
target_dir: &Path, target_dir: &Path,
package_mirror: Option<String>, mirror: Option<&str>,
md5_map: Option<&HashMap<String, String>>, md5_map: Option<&HashMap<String, String>>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<()> {
fs::create_dir_all(target_dir)?; fs::create_dir_all(target_dir)?;
let urls: Vec<&str> = wget_list.lines().filter(|l| !l.trim().is_empty()).collect();
let total = urls.len();
let client = Arc::new(Client::new());
let mp = Arc::new(MultiProgress::new());
let md5_map = md5_map.cloned();
let mut handles = vec![]; let mut handles = vec![];
for (i, url) in urls.into_iter().enumerate() { for url in files.iter().cloned() {
let client = Arc::clone(&client);
let mp = Arc::clone(&mp);
let target_dir = target_dir.to_path_buf(); let target_dir = target_dir.to_path_buf();
let package_mirror = package_mirror.clone(); let mirror = mirror.map(|m| m.to_string());
let url = url.to_string(); let md5_map = md5_map.cloned();
let md5_map = md5_map.clone();
let handle = thread::spawn(move || -> Result<(), Box<dyn std::error::Error + Send>> { let handle = thread::spawn(move || -> Result<()> {
let filename = url.split('/').next_back().unwrap_or("file.tar.xz"); let download_url = if let Some(m) = &mirror {
url.replace("ftp.gnu.org", m)
} else {
url.clone()
};
let filename = download_url
.split('/')
.last()
.ok_or_else(|| anyhow!("Failed to extract filename"))?;
let filepath = target_dir.join(filename); let filepath = target_dir.join(filename);
let download_url = if let Some(ref mirror) = package_mirror { let mut resp = reqwest::blocking::get(&download_url)?;
if url.contains("ftp.gnu.org") { let mut buffer = Vec::new();
url.replacen("ftp.gnu.org", mirror, 1) resp.read_to_end(&mut buffer)?;
} else {
url.to_string() let mut file = File::create(&filepath)?;
file.write_all(&buffer)?;
if let Some(md5s) = md5_map.as_ref() {
if let Some(expected) = md5s.get(filename) {
let digest = md5::compute(&buffer);
if format!("{:x}", digest) != *expected {
return Err(anyhow!("MD5 mismatch for {}", filename));
} }
} else {
url.to_string()
};
let pb = mp.add(ProgressBar::new(0));
pb.set_style(
ProgressStyle::with_template(
"{bar:40.cyan/blue} {bytes}/{total_bytes} ({eta}) {msg}",
)
.unwrap()
.progress_chars("=> "),
);
pb.set_message(format!(
"[{}/{}] {}",
i + 1,
total,
style(filename).yellow()
));
let mut resp = client
.get(&download_url)
.send()
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
let total_size = resp.content_length().unwrap_or(0);
pb.set_length(total_size);
let mut file = File::create(&filepath)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
let mut downloaded: u64 = 0;
let mut buffer = [0u8; 8192];
loop {
let bytes_read = resp
.read(&mut buffer)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
if bytes_read == 0 {
break;
} }
file.write_all(&buffer[..bytes_read])
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
} }
let status = if let Some(ref md5_map) = md5_map {
if let Some(expected_hash) = md5_map.get(filename) {
if verify_md5(&filepath, expected_hash) {
style("").green()
} else {
style("").red()
}
} else {
style("⚠️").yellow()
}
} else {
style("⚠️").yellow()
};
pb.finish_with_message(format!("{} {}", status, style(filename).yellow()));
Ok(()) Ok(())
}); });
@ -124,8 +56,7 @@ pub fn download_files(
} }
for handle in handles { for handle in handles {
let result = handle.join().unwrap(); handle.join().map_err(|_| anyhow!("Thread panicked"))??;
result.map_err(|e| e as Box<dyn std::error::Error>)?;
} }
Ok(()) Ok(())

View file

@ -1,33 +1,14 @@
use html_parser::Dom; use scraper::{Html, Selector};
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_pre_blocks(url: &str) -> anyhow::Result<Vec<String>> {
pub fn fetch_and_parse_html_to_json(url: &str) -> Result<String, Box<dyn Error>> { let body = reqwest::blocking::get(url)?.text()?;
// HTML herunterladen let document = Html::parse_document(&body);
let response = get(url)?; let selector = Selector::parse("pre").unwrap();
if !response.status().is_success() {
return Err(format!("Fehler beim Abrufen der URL {}: {}", url, response.status()).into()); let mut results = Vec::new();
for element in document.select(&selector) {
results.push(element.inner_html());
} }
let body = response.text()?; Ok(results)
// 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

@ -8,76 +8,16 @@ mod wget_list;
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
mod tui; mod tui;
use console::style;
use rand::Rng;
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
{ {
// TUI-Modus
tui::tui_menu()?; tui::tui_menu()?;
return Ok(()); return Ok(());
} }
#[cfg(not(feature = "tui"))] #[cfg(not(feature = "tui"))]
{ {
// --- Dynamische Version-Prüfung direkt aus HTML --- println!("TUI feature not enabled. Compile with `--features tui` to run TUI.");
let ok = version_check::run_version_checks_from_html(
"https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html",
)?;
if !ok {
eprintln!(
"{} Some version checks failed. Exiting.",
style("").red().bold()
);
std::process::exit(1);
}
println!(
"{} All version checks passed. Starting downloader...",
style("").green().bold()
);
// --- 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();
let random_number: u32 = rng.gen_range(1000..=9999);
let tmp_path = format!("/tmp/lfs_{}", random_number);
println!(
"{} Using temporary path {}",
style("").blue(),
style(&tmp_path).yellow()
);
PathBuf::from(tmp_path).join("sources")
}
};
// --- CLI Mirror-Auswahl: default oder erweiterbar ---
let package_mirror: Option<String> = None;
// --- Hole wget-Liste ---
let wget_list = wget_list::get_wget_list()?;
// --- 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() {
let mut parts = line.split_whitespace();
if let (Some(hash), Some(filename)) = (parts.next(), parts.next()) {
md5_map.insert(filename.to_string(), hash.to_string());
}
}
// --- Lade Dateien herunter ---
downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?;
println!("{} All done!", style("🎉").green().bold());
Ok(()) Ok(())
} }
} }

View file

@ -3,10 +3,14 @@ use reqwest::blocking::Client;
use reqwest::redirect::Policy; use reqwest::redirect::Policy;
pub fn get_md5sums() -> Result<String> { pub fn get_md5sums() -> Result<String> {
let client = Client::builder().redirect(Policy::none()).build()?; let client = Client::builder().redirect(Policy::limited(5)).build()?;
let res = client let res = client
.get("https://www.linuxfromscratch.org/~thomas/multilib-m32/md5sums") .get("https://www.linuxfromscratch.org/~thomas/multilib-m32/md5sums")
.send()? .send()?;
.text()?;
Ok(res) if !res.status().is_success() {
anyhow::bail!("Failed to fetch MD5sums: HTTP {}", res.status());
}
Ok(res.text()?)
} }

View file

@ -9,13 +9,14 @@ pub fn fetch_mirrors() -> Result<Vec<String>, Box<dyn std::error::Error>> {
.get("https://www.linuxfromscratch.org/lfs/mirrors.html#files") .get("https://www.linuxfromscratch.org/lfs/mirrors.html#files")
.send()? .send()?
.text()?; .text()?;
let document = Html::parse_document(&res); let document = Html::parse_document(&res);
let selector = Selector::parse("a[href^='http']").unwrap(); let selector = Selector::parse("a[href^='http']").unwrap();
let mirrors = document let mirrors = document
.select(&selector) .select(&selector)
.filter_map(|element| { .filter_map(|element| {
let href = element.value().attr("href")?; let href = element.value().attr("href")?;
// Basic filtering to get potential mirror URLs
if href.contains("ftp.gnu.org") || href.contains("mirror") { if href.contains("ftp.gnu.org") || href.contains("mirror") {
Some(href.to_string()) Some(href.to_string())
} else { } else {
@ -23,25 +24,20 @@ pub fn fetch_mirrors() -> Result<Vec<String>, Box<dyn std::error::Error>> {
} }
}) })
.collect(); .collect();
Ok(mirrors) Ok(mirrors)
} }
pub fn choose_package_mirror() -> Option<String> { pub fn choose_package_mirror() -> Option<String> {
let mirrors = match fetch_mirrors() { let mirrors = fetch_mirrors().unwrap_or_else(|_| {
Ok(mirrors) => mirrors,
Err(e) => {
println!("Failed to fetch mirrors: {}", e);
// Fallback to a default list if fetching fails
vec![ vec![
"ftp.fau.de".to_string(), "ftp.fau.de".to_string(),
"mirror.kernel.org".to_string(), "mirror.kernel.org".to_string(),
"mirror.example.org".to_string(), "mirror.example.org".to_string(),
] ]
} });
};
println!("Optional: choose a mirror for GNU source packages (replace ftp.gnu.org):");
println!("Optional: choose a mirror for GNU source packages:");
for (i, mirror) in mirrors.iter().enumerate() { for (i, mirror) in mirrors.iter().enumerate() {
println!(" [{}] {}", i + 1, mirror); println!(" [{}] {}", i + 1, mirror);
} }

View file

@ -1,417 +1,104 @@
#[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::{ use crossterm::{
event::{self, Event, KeyCode}, event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute, execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
}; };
#[cfg(feature = "tui")] use std::{error::Error, io};
use rand::Rng; use tui::{
#[cfg(feature = "tui")]
use ratatui::{
Terminal, Terminal,
backend::CrosstermBackend, backend::CrosstermBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, widgets::{Block, Borders, List, ListItem},
};
#[cfg(feature = "tui")]
use std::{
collections::HashMap,
fs,
io::stdout,
path::PathBuf,
sync::{Arc, Mutex},
thread,
time::{Duration, Instant},
}; };
#[cfg(feature = "tui")] pub fn tui_menu() -> Result<(), Box<dyn Error>> {
use reqwest::blocking::get; // Setup terminal
#[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::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");
fs::create_dir_all(&lfs_sources_path)?;
Ok(lfs_sources_path)
}
#[cfg(feature = "tui")]
fn select_mirrors_tui(mirrors: Vec<String>) -> Vec<String> {
if mirrors.is_empty() {
return vec![];
}
let mut selected: Vec<bool> = vec![false; mirrors.len()];
let mut state = ListState::default();
state.select(Some(0));
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen).unwrap();
enable_raw_mode().unwrap();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
loop {
terminal
.draw(|f| {
let size = f.size();
let items: Vec<ListItem> = mirrors
.iter()
.enumerate()
.map(|(i, mirror)| {
let prefix = if selected[i] { "[x] " } else { "[ ] " };
ListItem::new(format!("{}{}", prefix, mirror))
})
.collect();
let list = List::new(items)
.block(
Block::default()
.title("Select mirrors")
.borders(Borders::ALL),
)
.highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
if let Event::Key(key) = event::read().unwrap() {
match key.code {
KeyCode::Down => {
let i = state.selected().unwrap_or(0);
if i < mirrors.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::Char(' ') => {
let i = state.selected().unwrap_or(0);
selected[i] = !selected[i];
}
KeyCode::Enter => {
disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap();
return mirrors
.into_iter()
.enumerate()
.filter_map(|(i, m)| if selected[i] { Some(m) } else { None })
.collect();
}
KeyCode::Esc => {
disable_raw_mode().unwrap();
execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap();
return vec![];
}
_ => {}
}
}
}
}
#[cfg(feature = "tui")]
fn download_packages_tui(lfs_sources: &PathBuf) {
let mirrors_list = mirrors::fetch_mirrors().unwrap_or_default();
let selected_mirrors = select_mirrors_tui(mirrors_list);
if selected_mirrors.is_empty() {
return;
}
let wget_list_content = wget_list::get_wget_list().unwrap_or_default();
let wget_list: Vec<String> = wget_list_content.lines().map(|s| s.to_string()).collect();
if wget_list.is_empty() {
return;
}
let mut md5_map = HashMap::new();
if let Ok(md5_content) = md5_utils::get_md5sums() {
for line in md5_content.lines() {
if let Some((hash, filename)) = line.split_once(' ') {
md5_map.insert(filename.to_string(), hash.to_string());
}
}
}
let download_state: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(
wget_list
.iter()
.map(|f| (f.clone(), "Pending".into()))
.collect(),
));
let download_state_clone = Arc::clone(&download_state);
let mirrors_clone = selected_mirrors.clone();
let lfs_sources = lfs_sources.clone();
thread::spawn(move || {
for file in &wget_list {
let mut status = "Failed".to_string();
for mirror in &mirrors_clone {
if downloader::download_files(
file,
&lfs_sources,
Some(mirror.clone()),
Some(&md5_map),
)
.is_ok()
{
status = format!("Downloaded from {}", mirror);
break;
}
}
let mut state = download_state_clone.lock().unwrap();
if let Some(entry) = state.iter_mut().find(|(f, _)| f == file) {
entry.1 = status.clone();
}
}
});
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen).unwrap();
enable_raw_mode().unwrap();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
loop {
terminal
.draw(|f| {
let size = f.size();
let items: Vec<ListItem> = {
let state = download_state.lock().unwrap();
state
.iter()
.map(|(f, s)| ListItem::new(format!("{}: {}", f, s)))
.collect()
};
let list = List::new(items).block(
Block::default()
.title("Downloading Packages")
.borders(Borders::ALL),
);
f.render_widget(list, size);
})
.unwrap();
let state = download_state.lock().unwrap();
if state.iter().all(|(_, s)| s != "Pending") {
break;
}
thread::sleep(Duration::from_millis(100));
}
disable_raw_mode().unwrap();
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();
execute!(stdout, EnterAlternateScreen)?;
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let menu_items = vec![ let menu_items = vec!["Start downloader", "Exit"];
"🌱 Init environment", let mut selected = 0;
"📦 Download packages",
"🔍 Check status",
"🧹 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;
let mut status_message_timer: Option<Instant> = None;
loop { loop {
terminal.draw(|f| { terminal.draw(|f| {
let size = f.size(); let size = f.size();
let block = Block::default()
.title("✨ lpkg TUI 🌈")
.borders(Borders::ALL);
f.render_widget(block, size);
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(2) .margin(5)
.constraints(vec![Constraint::Length(3); menu_items.len()]) .constraints([Constraint::Length(menu_items.len() as u16)].as_ref())
.split(size); .split(size);
for (i, item) in menu_items.iter().enumerate() { let items: Vec<ListItem> = menu_items
let mut style = Style::default(); .iter()
if Some(i) == state.selected() { .enumerate()
style = style.bg(Color::Red).fg(Color::White); .map(|(i, m)| {
} let style = if i == selected {
let list_item = ListItem::new(*item).style(style); Style::default()
let list = List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)); .fg(Color::Yellow)
f.render_widget(list, chunks[i]); .add_modifier(Modifier::BOLD)
} } else {
Style::default()
};
ListItem::new(*m).style(style)
})
.collect();
if let Some(msg) = &status_message { let list = List::new(items).block(
let paragraph = Paragraph::new(msg.as_str()).block(
Block::default() Block::default()
.borders(Borders::NONE) .title("LFS Downloader")
.style(Style::default().fg(Color::Yellow)), .borders(Borders::ALL),
); );
let msg_area = Layout::default() f.render_widget(list, chunks[0]);
.direction(Direction::Horizontal)
.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];
f.render_widget(paragraph, msg_area);
}
})?; })?;
if let Some(timer) = status_message_timer { // Handle input
if timer.elapsed() > Duration::from_secs(3) {
status_message = None;
status_message_timer = None;
}
}
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { 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 => { KeyCode::Up => {
let i = state.selected().unwrap_or(0); if selected > 0 {
if i > 0 { selected -= 1;
state.select(Some(i - 1));
} }
} }
KeyCode::Enter => match state.selected() { KeyCode::Down => {
Some(0) => { if selected < menu_items.len() - 1 {
// Init environment selected += 1;
match init_environment() {
Ok(path) => {
lfs_sources = Some(path.clone());
status_message =
Some(format!("✅ Environment initialized: {}", path.display()));
}
Err(e) => {
status_message =
Some(format!("❌ Failed to init environment: {}", e));
} }
} }
status_message_timer = Some(Instant::now()); KeyCode::Enter => {
} match selected {
Some(1) => { 0 => {
// Download packages // Start downloader
if let Some(path) = &lfs_sources {
download_packages_tui(path);
}
}
Some(2) => {
// Status
status_message = Some("🔍 Status selected! (TODO)".to_string());
status_message_timer = Some(Instant::now());
}
Some(3) => {
// Cleanup
match cleanup_temp_directories() {
Ok(count) => {
status_message =
Some(format!("✅ Cleaned {} temporary dirs", count));
}
Err(e) => {
status_message = Some(format!("❌ Failed cleanup: {}", e));
}
}
status_message_timer = Some(Instant::now());
}
Some(4) => {
// Test JSON fetch
// Leave TUI first
disable_raw_mode()?; disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?; execute!(
terminal.show_cursor()?; terminal.backend_mut(),
LeaveAlternateScreen,
match fetch_and_parse_html_to_json( DisableMouseCapture
"https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html", )?;
) { super::start_downloader()?; // call your downloader function
Ok(json) => println!("✅ JSON output:\n{}", json),
Err(e) => eprintln!("❌ Error: {}", e),
}
return Ok(()); return Ok(());
} }
Some(5) | _ => break, 1 => {
}, break; // Exit
}
_ => {}
}
}
KeyCode::Esc => break, KeyCode::Esc => break,
_ => {} _ => {}
} }
} }
} }
// Restore terminal
disable_raw_mode()?; disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?; execute!(
terminal.show_cursor()?; terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
Ok(()) Ok(())
} }

View file

@ -1,9 +1,6 @@
use reqwest;
use scraper::{Html, Selector};
use semver::Version;
use std::process::Command; use std::process::Command;
/// Führt ein Kommando aus und gibt die erste Zeile der Version zurück /// Führt ein Kommando aus und gibt stdout zurück
fn run_command(cmd: &str, args: &[&str]) -> Option<String> { fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
let output = Command::new(cmd).args(args).output().ok()?; let output = Command::new(cmd).args(args).output().ok()?;
if output.status.success() { if output.status.success() {
@ -13,93 +10,22 @@ fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
} }
} }
/// Vergleicht zwei Versionen mit semver (für Programme) /// Vergleicht Versionen (semver für Programme)
fn check_version(installed: &str, required: &str) -> bool { fn check_version(installed: &str, required: &str) -> bool {
let i = Version::parse(installed).ok(); match (
let r = Version::parse(required).ok(); semver::Version::parse(installed),
match (i, r) { semver::Version::parse(required),
(Some(i), Some(r)) => i >= r, ) {
(Ok(i), Ok(r)) => i >= r,
_ => false, _ => false,
} }
} }
/// Vergleicht Kernel-Versionen (numerisch) /// Prüft einen <pre>-Block auf Versionen
fn check_kernel_version(installed: &str, required: &str) -> bool { pub fn run_version_checks_from_block(block: &str) -> bool {
let parse_ver = |v: &str| {
v.split(['.', '-'])
.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()
}
/// 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()
.next()
.unwrap_or("")
.split_whitespace()
.last()
.unwrap_or("");
if check_version(ver, min_version) {
println!("OK: {:<12} {:<8} >= {}", program, ver, min_version);
true
} else {
eprintln!(
"ERROR: {:<12} version {} is too old ({} required)",
program, ver, min_version
);
false
}
}
None => {
eprintln!("ERROR: Cannot find {}", program);
false
}
}
}
/// 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_kernel_version(&kernel, min_version) {
println!("OK: Linux Kernel {} >= {}", kernel, min_version);
true
} else {
eprintln!(
"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<bool, 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; let mut ok = true;
for element in document.select(&selector) { for line in block.lines() {
let pre_text = element.text().collect::<Vec<_>>().join("\n");
for line in pre_text.lines() {
let line = line.trim(); let line = line.trim();
if line.starts_with("ver_check") { if line.starts_with("ver_check") {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
@ -107,51 +33,41 @@ pub fn run_version_checks_from_html(url: &str) -> Result<bool, Box<dyn std::erro
let prog = parts[1]; let prog = parts[1];
let cmd = parts[2]; let cmd = parts[2];
let ver = parts[3]; let ver = parts[3];
ok &= ver_check(prog, cmd, ver); let installed = run_command(cmd, &["--version"]).unwrap_or_default();
let ver_inst = installed
.lines()
.next()
.unwrap_or("")
.split_whitespace()
.last()
.unwrap_or("");
if check_version(ver_inst, ver) {
println!("OK: {} {} >= {}", prog, ver_inst, ver);
} else {
eprintln!("ERROR: {} {} < {}", prog, ver_inst, ver);
ok = false;
}
} }
} else if line.starts_with("ver_kernel") { } else if line.starts_with("ver_kernel") {
if let Some(ver) = line.split_whitespace().nth(1) { if let Some(ver) = line.split_whitespace().nth(1) {
ok &= ver_kernel(ver); let kernel = run_command("uname", &["-r"]).unwrap_or_default();
} let installed = kernel
} .split(['-', '.'])
} .filter_map(|s| s.parse::<u32>().ok())
} .collect::<Vec<_>>();
let required = ver
// Alias-Checks .split(['-', '.'])
let alias_check = |cmd: &str, expected: &str| { .filter_map(|s| s.parse::<u32>().ok())
if let Some(output) = run_command(cmd, &["--version"]) { .collect::<Vec<_>>();
if output.to_lowercase().contains(&expected.to_lowercase()) { if installed >= required {
println!("OK: {:<4} is {}", cmd, expected); println!("OK: Linux Kernel {} >= {}", kernel, ver);
} else { } else {
eprintln!("ERROR: {:<4} is NOT {}", cmd, expected); eprintln!("ERROR: Linux Kernel {} < {}", kernel, ver);
}
}
};
alias_check("awk", "GNU");
alias_check("yacc", "Bison");
alias_check("sh", "Bash");
// Compiler-Test
if run_command("g++", &["--version"]).is_some() {
println!("OK: g++ works");
} else {
eprintln!("ERROR: g++ does NOT work");
ok = false; ok = false;
} }
}
// nproc-Test }
let nproc = run_command("nproc", &[]).unwrap_or_default();
if nproc.is_empty() {
eprintln!("ERROR: nproc is not available or empty");
ok = false;
} else {
println!("OK: nproc reports {} logical cores available", nproc);
} }
if !ok { ok
eprintln!("Some version checks failed.");
}
Ok(ok)
} }

View file

@ -3,10 +3,14 @@ use reqwest::blocking::Client;
use reqwest::redirect::Policy; use reqwest::redirect::Policy;
pub fn get_wget_list() -> Result<String> { pub fn get_wget_list() -> Result<String> {
let client = Client::builder().redirect(Policy::none()).build()?; let client = Client::builder().redirect(Policy::limited(5)).build()?;
let res = client let res = client
.get("https://www.linuxfromscratch.org/~thomas/multilib-m32/wget-list-sysv") .get("https://www.linuxfromscratch.org/~thomas/multilib-m32/wget-list-sysv")
.send()? .send()?;
.text()?;
Ok(res) if !res.status().is_success() {
anyhow::bail!("Failed to fetch wget-list: HTTP {}", res.status());
}
Ok(res.text()?)
} }