From 311304666f8cab1364968cf72ff6186acdea22a9 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Thu, 27 Nov 2025 19:34:46 +0100 Subject: [PATCH] better logging Use callbacks to report progress, indicatif for progress bars, no direct logs but progress messages --- Cargo.toml | 8 ++- src/get.rs | 132 +++++++++++++++++++++++++++++++------------- src/lib.rs | 2 + src/main.rs | 61 ++++++++++++++++---- src/package_info.rs | 12 ++-- 5 files changed, 158 insertions(+), 57 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 396cb97..67a15a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,18 @@ cmd_lib = "2.0.0" flate2 = "1.1.5" serde = { version = "1.0.228", features = ["derive"] } csv = "1.3.0" -reqwest = { version = "0.12.9", features = ["blocking", "json"] } +reqwest = { version = "0.12.9", features = ["blocking", "json", "stream"] } git2 = "0.19.0" regex = "1" chrono = "0.4" tokio = { version = "1.41.1", features = ["full"] } sha2 = "0.10.8" hex = "0.4.3" -tracing = "0.1.42" +log = "0.4.28" +indicatif = "0.17" +indicatif-log-bridge = "0.2.3" +env_logger = "0.11.8" +futures-util = { version = "0.3.31", features = ["tokio-io"] } [dev-dependencies] tempfile = "3.10.1" diff --git a/src/get.rs b/src/get.rs index c890a7b..c3d16f7 100644 --- a/src/get.rs +++ b/src/get.rs @@ -1,69 +1,117 @@ -use serde::Deserialize; use std::error::Error; use std::path::Path; +use std::cmp::min; use pkh::package_info; use pkh::package_info::PackageInfo; use std::process::Command; -fn clone_repo(url: &str, package: &str, cwd: Option<&Path>) -> Result<(), Box> { - println!("Cloning {} into {}...", url, package); - +use log::{debug}; + +use regex::Regex; + +use pkh::ProgressCallback; + +fn clone_repo(url: &str, package: &str, cwd: Option<&Path>, progress: ProgressCallback<'_>) -> Result<(), Box> { let target_path = if let Some(path) = cwd { path.join(package) } else { Path::new(package).to_path_buf() }; + + let mut callbacks = git2::RemoteCallbacks::new(); + if let Some(ref progress_cb) = progress { + callbacks.transfer_progress(move |stats| { + (progress_cb)("", "Receiving objects...", stats.received_objects(), stats.total_objects()); + true + }); + callbacks.sideband_progress(move |data| { + let msg = String::from_utf8_lossy(data); + let re = Regex::new(r"(.*):[ ]*([0-9]*)% \(([0-9]*)/([0-9]*)\)").unwrap(); + if let Some(caps) = re.captures(msg.trim()) { + let msg = caps.get(1).map_or("", |m| m.as_str()).to_string(); + let objects = caps.get(3).map_or("", |m| m.as_str()).to_string().parse::().unwrap_or(0); + let total = caps.get(4).map_or("", |m| m.as_str()).to_string().parse::().unwrap_or(0); - git2::Repository::clone(url, target_path)?; - Ok(()) + (progress_cb)("", msg.as_str(), objects, total); + } + + true + }); + } + + let mut fetch_options = git2::FetchOptions::new(); + fetch_options.remote_callbacks(callbacks); + + let mut builder = git2::build::RepoBuilder::new(); + builder.fetch_options(fetch_options); + + return match builder.clone(url, &target_path) { + Ok(_repo) => { + Ok(()) + } + Err(e) => { + Err(format!("Failed to clone: {}", e).into()) + } + }; } use sha2::{Sha256, Digest}; use std::fs::File; use std::io::Write; +use futures_util::StreamExt; + fn checkout_pristine_tar(package_dir: &Path, filename: &str) -> Result<(), Box> { - println!("Attempting to checkout {} using pristine-tar...", filename); - let status = Command::new("pristine-tar") + let output = Command::new("pristine-tar") .current_dir(package_dir) .args(&["checkout", format!("../{filename}").as_str()]) - .status()?; + .output() + .expect("pristine-tar checkout failed"); - if !status.success() { - return Err(format!("pristine-tar checkout failed with status: {}", status).into()); + if !output.status.success() { + return Err(format!("pristine-tar checkout failed with status: {}", output.status).into()); } Ok(()) } -async fn download_file_checksum(url: &str, checksum: &str, target_dir: &Path) -> Result<(), Box> { +async fn download_file_checksum(url: &str, checksum: &str, target_dir: &Path, progress: ProgressCallback<'_>) -> Result<(), Box> { // Download with reqwest let response = reqwest::get(url).await?; if !response.status().is_success() { - return Err(format!("Failed to download: {}", response.status()).into()); + return Err(format!("Failed to download '{}' : {}", &url, response.status()).into()); } + + let total_size = response.content_length().ok_or(format!("Failed to get content length from '{}'", &url))?; + let mut index = 0; - let content = response.bytes().await?; + // Target file: extract file name from URL + let filename = Path::new(url).file_name().unwrap().to_str().unwrap(); + let path = target_dir.join(filename); + let mut file = File::create(path)?; - // Verify checksum + // Download chunk by chunk to disk, while updating hasher for checksum + let mut stream = response.bytes_stream(); let mut hasher = Sha256::new(); - hasher.update(&content); + while let Some(item) = stream.next().await { + let chunk = item?; + file.write_all(&chunk)?; + hasher.update(&chunk); + + if let Some(cb) = progress { + index = min(index + &chunk.len(), total_size as usize); + cb("", "Downloading...", index, total_size as usize); + } + } + + // Verify checksum let result = hasher.finalize(); let calculated_checksum = hex::encode(result); - if calculated_checksum != checksum { return Err(format!("Checksum mismatch! Expected {}, got {}", checksum, calculated_checksum).into()); } - // Extract file name from URL - let filename = Path::new(url).file_name().unwrap().to_str().unwrap(); - - // Write to disk - let path = target_dir.join(filename); - let mut file = File::create(path)?; - file.write_all(&content)?; - Ok(()) } @@ -81,7 +129,7 @@ fn setup_pristine_tar_branch(package_dir: &Path, dist: &str) -> Result<(), Box Result<(), Box) -> Result<(), Box> { +async fn fetch_orig_tarball(info: &PackageInfo, cwd: Option<&Path>, progress: ProgressCallback<'_>) -> Result<(), Box> { let package_dir = if let Some(path) = cwd { path.join(&info.stanza.package) } else { @@ -118,22 +166,25 @@ async fn fetch_orig_tarball(info: &PackageInfo, cwd: Option<&Path>) -> Result<() // 1. Try executing pristine-tar // Setup pristine-tar branch if needed (by tracking remote branch) - setup_pristine_tar_branch(&package_dir, info.dist.as_str()); + let _ = setup_pristine_tar_branch(&package_dir, info.dist.as_str()); if let Err(e) = checkout_pristine_tar(&package_dir, filename.as_str()) { - println!("pristine-tar failed: {}. Falling back to archive download.", e); + debug!("pristine-tar failed: {}. Falling back to archive download.", e); // 2. Fallback to archive download // We download to the parent directory of the package repo (which is standard for build tools) // or the current directory if cwd is None (which effectively is the parent of the package dir) let target_dir = cwd.unwrap_or_else(|| Path::new(".")); - download_file_checksum(format!("{}/{}", &info.archive_url, filename).as_str(), &orig_file.sha256, target_dir).await?; + download_file_checksum(format!("{}/{}", &info.archive_url, filename).as_str(), &orig_file.sha256, target_dir, progress).await?; } Ok(()) } -pub async fn get(package: &str, _version: &str, series: &str, pocket: &str, _ppa: &str, cwd: Option<&Path>) -> Result<(), Box> { +pub async fn get(package: &str, _version: &str, series: &str, pocket: &str, _ppa: &str, cwd: Option<&Path>, progress: ProgressCallback<'_>) -> Result<(), Box> { + if let Some(cb) = progress { + cb(&format!("Resolving package info for {}...", package), "", 0, 0); + } let package_info = package_info::get(package, series, pocket).await; let package_dir = if let Some(path) = cwd { @@ -144,11 +195,18 @@ pub async fn get(package: &str, _version: &str, series: &str, pocket: &str, _ppa if let Ok(Some(info)) = package_info { if let Some(ref url) = info.preferred_vcs { - clone_repo(url.as_str(), package, Some(&package_dir))?; - fetch_orig_tarball(&info, Some(&package_dir)).await?; + if let Some(cb) = progress { + cb(&format!("Cloning {}...", url), "", 0, 0); + } + clone_repo(url.as_str(), package, Some(&package_dir), progress)?; + + if let Some(cb) = progress { + cb("Fetching orig tarball...", "", 0, 0); + } + fetch_orig_tarball(&info, Some(&package_dir), progress).await?; } } else { - println!("No VCS URL found for package {}", package); + return Err(format!("No VCS URL found for package {}", package).into()); } Ok(()) @@ -166,7 +224,7 @@ mod tests { let cwd = temp_dir.path(); // Main 'get' command: the one we want to test - get(package, "", series, "", "", Some(cwd)).await.unwrap(); + get(package, "", series, "", "", Some(cwd), None).await.unwrap(); let package_dir = cwd.join(package); assert!(package_dir.exists(), "Package directory not created"); diff --git a/src/lib.rs b/src/lib.rs index 124d198..abe4837 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,3 @@ pub mod package_info; + +pub type ProgressCallback<'a> = Option<&'a dyn Fn(&str, &str, usize, usize)>; diff --git a/src/main.rs b/src/main.rs index 96b3ed6..7f62105 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,34 @@ use std::env; -use std::error::Error; -use std::collections::HashMap; - -extern crate serde; -use serde::Deserialize; +use std::io::Write; +use std::time::Duration; extern crate clap; -use clap::{arg, command, value_parser, ArgAction, Command}; +use clap::{arg, command, Command}; extern crate flate2; -extern crate cmd_lib; -use cmd_lib::{run_cmd}; - mod get; use get::get; mod changelog; use changelog::generate_entry; +use log::{info, error}; +use indicatif_log_bridge::LogWrapper; + fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); + let logger = + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format_timestamp(None) + .format(|buf, record| { + writeln!(buf, "{}", record.args()) + }) + .build(); + let multi = indicatif::MultiProgress::new(); + LogWrapper::new(multi.clone(), logger) + .try_init() + .unwrap(); let matches = command!() .subcommand_required(true) .disable_version_flag(true) @@ -59,17 +67,46 @@ fn main() { let ppa = sub_matches.get_one::("ppa").map(|s| s.as_str()).unwrap_or(""); // Since get is async, we need to block on it - if let Err(e) = rt.block_on(get(package, version, series, "", ppa, None)) { - eprintln!("Error: {}", e); + let pb = multi.add(indicatif::ProgressBar::new(0)); + pb.enable_steady_tick(Duration::from_millis(50)); + + let mut progress_callback = |prefix: &str, msg: &str, progress: usize, total: usize| { + + if progress != 0 && total != 0 { + pb.set_style(indicatif::ProgressStyle::default_bar() + .template("> {spinner:.blue} {prefix}\n {msg} [{bar:40.cyan/blue}] {pos}/{len} ({eta})") + .unwrap() + .progress_chars("=> ")); + } else { + pb.set_style(indicatif::ProgressStyle::default_bar() + .template("> {spinner:.blue} {prefix}") + .unwrap()); + } + + if ! prefix.is_empty() { + pb.set_prefix(prefix.to_string()); + } + + pb.set_message(msg.to_string()); + pb.set_length(total as u64); + pb.set_position(progress as u64); + }; + + if let Err(e) = rt.block_on(get(package, version, series, "", ppa, None, Some(&mut progress_callback))) { + pb.finish_and_clear(); + error!("{}", e); std::process::exit(1); } + pb.finish_and_clear(); + multi.remove(&pb); + info!("Done."); }, Some(("chlog", sub_matches)) => { let cwd = std::env::current_dir().unwrap(); let version = sub_matches.get_one::("version").map(|s| s.as_str()); if let Err(e) = generate_entry("debian/changelog", Some(&cwd), version) { - eprintln!("Error: {}", e); + error!("{}", e); std::process::exit(1); } diff --git a/src/package_info.rs b/src/package_info.rs index 0733f6e..b394615 100644 --- a/src/package_info.rs +++ b/src/package_info.rs @@ -1,10 +1,11 @@ use flate2::read::GzDecoder; use std::io::Read; use std::collections::HashMap; -use serde::Deserialize; use std::error::Error; use std::path::Path; +use log::debug; + const BASE_URL_UBUNTU: &str = "http://archive.ubuntu.com/ubuntu"; const BASE_URL_DEBIAN: &str = "http://deb.debian.org/debian"; @@ -185,7 +186,7 @@ pub async fn get(package_name: &str, series: &str, pocket: &str) -> Result Result Result