This commit is contained in:
Lucy 2025-09-30 16:51:03 +02:00
commit 0d177729ab
15 changed files with 2263 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

13
.github/workflows/build_nix.yml vendored Normal file
View file

@ -0,0 +1,13 @@
name: "Build legacy Nix package on Ubuntu"
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v26
- name: Building package
run: nix build

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug
target
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

1703
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "package_management"
version = "0.1.0"
edition = "2024"
[dependencies]
console = "0.16.1"
indicatif = "0.18.0"
md5 = "0.8.0"
rand = "0.9.2"
reqwest = { version = "0.12.23", features = ["blocking", "json"] }
serde = { version = "1.0.228", features = ["derive"] }

7
default.nix Normal file
View file

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).defaultNix

137
flake.lock generated Normal file
View file

@ -0,0 +1,137 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"naersk",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1752475459,
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"naersk": {
"inputs": {
"fenix": "fenix",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1752689277,
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
"owner": "nix-community",
"repo": "naersk",
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1752077645,
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1759070547,
"narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "647e5c14cbd5067f44ac86b74f014962df460840",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1752428706,
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

40
flake.nix Normal file
View file

@ -0,0 +1,40 @@
{
inputs = {
naersk.url = "github:nix-community/naersk/master";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
utils,
naersk,
}:
utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
naersk-lib = pkgs.callPackage naersk { };
in
{
defaultPackage = naersk-lib.buildPackage ./.;
devShell =
with pkgs;
mkShell {
buildInputs = [
cargo
rustc
rustfmt
pre-commit
rustPackages.clippy
pkg-config
openssl
gemini-cli-bin
];
RUST_SRC_PATH = rustPlatform.rustLibSrc;
};
}
);
}

7
shell.nix Normal file
View file

@ -0,0 +1,7 @@
(import (
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz";
sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }
) {
src = ./.;
}).shellNix

137
src/downloader.rs Normal file
View file

@ -0,0 +1,137 @@
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use std::sync::Arc;
use std::thread;
use console::style;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use md5;
use reqwest::blocking::Client;
/// Prüft Datei gegen erwarteten MD5-Hash
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
}
/// Download + Live-MD5-Prüfung
pub fn download_files(
wget_list: &str,
target_dir: &Path,
package_mirror: Option<String>,
md5_map: Option<&HashMap<String, String>>,
) -> Result<(), Box<dyn std::error::Error>> {
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());
// Clone md5_map before the loop so we can move it into threads
let md5_map = md5_map.cloned();
let mut handles = vec![];
for (i, url) in urls.into_iter().enumerate() {
let client = Arc::clone(&client);
let mp = Arc::clone(&mp);
let target_dir = target_dir.to_path_buf();
let package_mirror = package_mirror.clone();
let url = url.to_string();
let md5_map = md5_map.clone();
let handle = thread::spawn(move || -> Result<(), Box<dyn std::error::Error + Send>> {
let filename = url.split('/').last().unwrap_or("file.tar.xz");
let filepath = target_dir.join(filename);
let download_url = if let Some(ref mirror) = package_mirror {
if url.contains("ftp.gnu.org") {
url.replacen("ftp.gnu.org", mirror, 1)
} else {
url.to_string()
}
} 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);
}
// Live-MD5-Prüfung
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(())
});
handles.push(handle);
}
for handle in handles {
let result = handle.join().unwrap();
result.map_err(|e| e as Box<dyn std::error::Error>)?;
}
Ok(())
}

50
src/main.rs Normal file
View file

@ -0,0 +1,50 @@
mod downloader;
mod md5_utils;
mod mirrors;
mod wget_list;
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>> {
// LFS sources Pfad
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 tmp_path = format!("/tmp/lfs_{}", random_number);
println!(
"{} Using temporary path {}",
style("").blue(),
style(&tmp_path).yellow()
);
PathBuf::from(tmp_path).join("sources")
}
};
// Mirror für Pakete auswählen
let package_mirror = mirrors::choose_package_mirror();
// Wget-Liste vom Original LFS-Mirror holen
let wget_list = wget_list::get_wget_list()?;
// MD5 Map vorbereiten
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());
}
}
// Pakete herunterladen + Live-MD5 prüfen
downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?;
println!("{} All done!", style("🎉").green().bold());
Ok(())
}

59
src/md5_utils.rs Normal file
View file

@ -0,0 +1,59 @@
use console::style;
use md5;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
pub fn get_md5sums() -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let res = client
.get("https://www.linuxfromscratch.org/~thomas/multilib-m32/md5sums")
.send()?
.text()?;
Ok(res)
}
pub fn save_md5sums(content: &str, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
std::fs::write(path, content)?;
Ok(())
}
pub fn verify_md5sums(
md5_file: &Path,
sources_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(md5_file)?;
for line in BufReader::new(file).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let mut parts = line.split_whitespace();
let expected_hash = parts.next().ok_or("Malformed md5sums line")?;
let filename = parts.next().ok_or("Malformed md5sums line")?;
let file_path = sources_dir.join(filename);
let mut f = File::open(&file_path)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
let digest = md5::compute(&buffer);
let hex = format!("{:x}", digest);
if hex == expected_hash {
println!("{} {} OK", style("").green(), filename);
} else {
println!(
"{} {} FAILED (expected {}, got {})",
style("").red(),
filename,
expected_hash,
hex
);
}
}
Ok(())
}

33
src/mirrors Normal file
View file

@ -0,0 +1,33 @@
use console::Style;
use std::io::{self, Write};
pub fn choose_package_mirror() -> Option<String> {
let mirrors = vec![
"https://ftp.fau.de",
"https://mirror.kernel.org/linux",
"https://mirror.example.org/linux",
];
println!("Optional: choose a mirror for source packages:");
for (i, mirror) in mirrors.iter().enumerate() {
println!(" [{}] {}", i + 1, mirror);
}
print!("Enter number or press Enter for default: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input.is_empty() {
None
} else {
let choice = input.parse::<usize>().unwrap_or(1);
let chosen = mirrors.get(choice.saturating_sub(1)).unwrap_or(&mirrors[0]);
println!("Using package mirror: {}", Style::new().green().apply_to(chosen));
Some(chosen.to_string())
}
}

31
src/mirrors.rs Normal file
View file

@ -0,0 +1,31 @@
use console::Style;
use std::io::{self, Write};
pub fn choose_package_mirror() -> Option<String> {
let mirrors = vec!["ftp.fau.de", "mirror.kernel.org", "mirror.example.org"];
println!("Optional: choose a mirror for GNU source packages (replace ftp.gnu.org):");
for (i, mirror) in mirrors.iter().enumerate() {
println!(" [{}] {}", i + 1, mirror);
}
print!("Enter number or press Enter for default: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input.is_empty() {
None
} else {
let choice = input.parse::<usize>().unwrap_or(1);
let chosen = mirrors.get(choice.saturating_sub(1)).unwrap_or(&mirrors[0]);
println!(
"Using package mirror: {}",
Style::new().green().apply_to(chosen)
);
Some(chosen.to_string())
}
}

11
src/wget_list.rs Normal file
View file

@ -0,0 +1,11 @@
use reqwest::blocking::Client;
use reqwest::redirect::Policy;
pub fn get_wget_list() -> Result<String, Box<dyn std::error::Error>> {
let client = Client::builder().redirect(Policy::none()).build()?;
let res = client
.get("https://www.linuxfromscratch.org/~thomas/multilib-m32/wget-list-sysv")
.send()?
.text()?;
Ok(res)
}