From f52281d6918e3842b8716756570f7e35e8b3fe18 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Mon, 5 May 2025 02:30:46 +0300 Subject: [PATCH] main: separate logging channels via `log` crate --- Cargo.lock | 90 ++++++++++++++++++++ Cargo.toml | 4 +- src/main.rs | 231 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 266 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 526dade..7739484 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "heck" version = "0.5.0" @@ -119,6 +142,36 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "jiff" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memchr" version = "2.7.4" @@ -130,6 +183,8 @@ name = "nnpdt" version = "0.1.0" dependencies = [ "clap", + "env_logger", + "log", "regex", "thiserror", "yansi", @@ -141,6 +196,21 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -188,6 +258,26 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index b24c26a..0210c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,7 @@ edition = "2024" [dependencies] clap = { version = "4.5.37", features = ["derive"] } regex = "1.11.1" -thiserror = "2.0.12" yansi = "1.0.1" +thiserror = "2.0.12" +log = "0.4.20" +env_logger = "0.11.3" diff --git a/src/main.rs b/src/main.rs index d235e2c..7b5cfdd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ use clap::Parser; use core::str; +use env_logger; +use log::{debug, error, info, warn}; use regex::Regex; use std::{ collections::{HashMap, HashSet}, process::Command, - string::String, + string::{String, ToString}, sync::OnceLock, thread, }; @@ -49,6 +51,14 @@ struct Args { /// Print the closure size #[arg(long, short)] closure_size: bool, + + /// Verbosity level: -v for debug, -vv for trace + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, + + /// Silence all output except errors + #[arg(short, long)] + quiet: bool, } #[derive(Debug, Clone)] @@ -78,14 +88,35 @@ impl<'a> Package<'a> { fn main() { let args = Args::parse(); - println!("Nix available: {}", check_nix_available()); + // Configure logger based on verbosity flags and environment variables + // Respects RUST_LOG environment variable if present. + // XXX:We can also dedicate a specific env variable for this tool, if we want to. + let env = env_logger::Env::default().filter_or( + "RUST_LOG", + if args.quiet { + "error" + } else { + match args.verbose { + 0 => "info", + 1 => "debug", + _ => "trace", + } + }, + ); + // Build and initialize the logger + env_logger::Builder::from_env(env) + .format_timestamp(Some(env_logger::fmt::TimestampPrecision::Seconds)) + .init(); + + debug!("Nix available: {}", check_nix_available()); // XXX: is this supposed to be user-facing? println!("<<< {}", args.path.to_string_lossy()); println!(">>> {}", args.path2.to_string_lossy()); - // Handles to the threads collecting closure size information + // handles to the threads collecting closure size information // We do this as early as possible because nix is slow. let closure_size_handles = if args.closure_size { + debug!("Calculating closure sizes in background"); let path = args.path.clone(); let path2 = args.path2.clone(); Some(( @@ -98,17 +129,41 @@ fn main() { // Get package lists and handle potential errors let package_list_pre = match get_packages(&args.path) { - Ok(packages) => packages, + Ok(packages) => { + debug!("Found {} packages in first closure", packages.len()); + packages + } Err(e) => { - eprintln!("Error getting packages from path {}: {}", args.path.display(), e); + error!( + "Error getting packages from path {}: {}", + args.path.display(), + e + ); + eprintln!( + "Error getting packages from path {}: {}", + args.path.display(), + e + ); Vec::new() } }; let package_list_post = match get_packages(&args.path2) { - Ok(packages) => packages, + Ok(packages) => { + debug!("Found {} packages in second closure", packages.len()); + packages + } Err(e) => { - eprintln!("Error getting packages from path {}: {}", args.path2.display(), e); + error!( + "Error getting packages from path {}: {}", + args.path2.display(), + e + ); + eprintln!( + "Error getting packages from path {}: {}", + args.path2.display(), + e + ); Vec::new() } }; @@ -121,11 +176,9 @@ fn main() { match get_version(&**p) { Ok((name, version)) => { pre.entry(name).or_default().insert(version); - }, + } Err(e) => { - if cfg!(debug_assertions) { - eprintln!("Error parsing package version: {e}"); - } + debug!("Error parsing package version: {}", e); } } } @@ -134,11 +187,9 @@ fn main() { match get_version(&**p) { Ok((name, version)) => { post.entry(name).or_default().insert(version); - }, + } Err(e) => { - if cfg!(debug_assertions) { - eprintln!("Error parsing package version: {e}"); - } + debug!("Error parsing package version: {}", e); } } } @@ -153,6 +204,20 @@ fn main() { // Get the intersection of the package names for version changes let changed: HashSet<&str> = &pre_keys & &post_keys; + debug!("Added packages: {}", added.len()); + debug!("Removed packages: {}", removed.len()); + debug!( + "Changed packages: {}", + changed + .iter() + .filter(|p| !p.is_empty()) + .filter_map(|p| match (pre.get(p), post.get(p)) { + (Some(ver_pre), Some(ver_post)) if ver_pre != ver_post => Some(p), + _ => None, + }) + .count() + ); + println!("Difference between the two generations:"); println!(); @@ -165,15 +230,20 @@ fn main() { if let Some((pre_handle, post_handle)) = closure_size_handles { match (pre_handle.join(), post_handle.join()) { (Ok(Ok(pre_size)), Ok(Ok(post_size))) => { + debug!("Pre closure size: {} MiB", pre_size); + debug!("Post closure size: {} MiB", post_size); + println!("{}", "Closure Size:".underline().bold()); println!("Before: {pre_size} MiB"); println!("After: {post_size} MiB"); println!("Difference: {} MiB", post_size - pre_size); } (Ok(Err(e)), _) | (_, Ok(Err(e))) => { + error!("Error getting closure size: {}", e); eprintln!("Error getting closure size: {e}"); } _ => { + error!("Failed to get closure size information due to a thread error"); eprintln!("Error: Failed to get closure size information due to a thread error"); } } @@ -182,6 +252,8 @@ fn main() { /// Gets the packages in a closure fn get_packages(path: &std::path::Path) -> Result> { + debug!("Getting packages from path: {}", path.display()); + // Get the nix store paths using `nix-store --query --references `` let output = Command::new("nix-store") .arg("--query") @@ -191,18 +263,23 @@ fn get_packages(path: &std::path::Path) -> Result> { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); + error!("nix-store command failed: {}", stderr); return Err(AppError::CommandFailed(format!( "nix-store command failed: {stderr}" ))); } let list = str::from_utf8(&output.stdout)?; - Ok(list.lines().map(str::to_owned).collect()) + let packages: Vec = list.lines().map(str::to_owned).collect(); + debug!("Found {} packages", packages.len()); + Ok(packages) } /// Gets the dependencies of the packages in a closure fn get_dependencies(path: &std::path::Path) -> Result> { - // Get the nix store paths using `nix-store --query --requisites `` + debug!("Getting dependencies from path: {}", path.display()); + + // Get the nix store paths using `nix-store --query --requisites ` let output = Command::new("nix-store") .arg("--query") .arg("--requisites") @@ -211,13 +288,16 @@ fn get_dependencies(path: &std::path::Path) -> Result> { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); + error!("nix-store command failed: {}", stderr); return Err(AppError::CommandFailed(format!( "nix-store command failed: {stderr}" ))); } let list = str::from_utf8(&output.stdout)?; - Ok(list.lines().map(str::to_owned).collect()) + let dependencies: Vec = list.lines().map(str::to_owned).collect(); + debug!("Found {} dependencies", dependencies.len()); + Ok(dependencies) } // Returns a reference to the compiled regex pattern. @@ -256,13 +336,21 @@ fn get_version<'a>(pack: impl Into<&'a str>) -> Result<(&'a str, &'a str)> { fn check_nix_available() -> bool { // Check if nix is available on the host system. + debug!("Checking nix command availability"); let nix_available = Command::new("nix").arg("--version").output().ok(); let nix_query_available = Command::new("nix-store").arg("--version").output().ok(); - nix_available.is_some() && nix_query_available.is_some() + let result = nix_available.is_some() && nix_query_available.is_some(); + if !result { + warn!("Nix commands not available, functionality may be limited"); + } + + result } fn get_closure_size(path: &std::path::Path) -> Result { + debug!("Calculating closure size for path: {}", path.display()); + // Run nix path-info command to get closure size let output = Command::new("nix") .arg("path-info") @@ -272,6 +360,7 @@ fn get_closure_size(path: &std::path::Path) -> Result { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); + error!("nix path-info command failed: {}", stderr); return Err(AppError::CommandFailed(format!( "nix path-info command failed: {stderr}" ))); @@ -280,20 +369,33 @@ fn get_closure_size(path: &std::path::Path) -> Result { let stdout = str::from_utf8(&output.stdout)?; // Parse the last word in the output as an integer (in bytes) - stdout + let size = stdout .split_whitespace() .last() - .ok_or_else(|| AppError::ParseError("Unexpected output format from nix path-info".into()))? + .ok_or_else(|| { + let err = "Unexpected output format from nix path-info"; + error!("{}", err); + AppError::ParseError(err.into()) + })? .parse::() - .map_err(|e| AppError::ParseError(format!("Failed to parse size value: {e}"))) - .map(|size| size / 1024 / 1024) // Convert to MiB + .map_err(|e| { + let err = format!("Failed to parse size value: {e}"); + error!("{}", err); + AppError::ParseError(err) + })?; + + // Convert to MiB + let size_mib = size / 1024 / 1024; + debug!("Closure size for {}: {} MiB", path.display(), size_mib); + Ok(size_mib) } fn print_added(set: HashSet<&str>, post: &HashMap<&str, HashSet<&str>>) { println!("{}", "Packages added:".underline().bold()); - // Use sorted output - let mut sorted: Vec<_> = set.iter() + // Use sorted outpu + let mut sorted: Vec<_> = set + .iter() .filter_map(|p| post.get(p).map(|ver| (*p, ver))) .collect(); @@ -314,18 +416,26 @@ fn print_added(set: HashSet<&str>, post: &HashMap<&str, HashSet<&str>>) { fn print_removed(set: HashSet<&str>, pre: &HashMap<&str, HashSet<&str>>) { println!("{}", "Packages removed:".underline().bold()); - set.iter() + + // Use sorted output for more predictable and readable results + let mut sorted: Vec<_> = set + .iter() .filter_map(|p| pre.get(p).map(|ver| (*p, ver))) - .for_each(|(p, ver)| { - let version_str = ver.iter().copied().collect::>().join(" "); - println!( - "{} {} {} {}", - "[R:]".red().bold(), - p, - "@".yellow(), - version_str.blue() - ); - }); + .collect(); + + // Sort by package name for consistent output + sorted.sort_by(|(a, _), (b, _)| a.cmp(b)); + + for (p, ver) in sorted { + let version_str = ver.iter().copied().collect::>().join(" "); + println!( + "{} {} {} {}", + "[R:]".red().bold(), + p, + "@".yellow(), + version_str.blue() + ); + } } fn print_changes( @@ -334,27 +444,32 @@ fn print_changes( post: &HashMap<&str, HashSet<&str>>, ) { println!("{}", "Version changes:".underline().bold()); - set.iter() - .filter(|p| !p.is_empty()) - .filter_map(|p| { - match (pre.get(p), post.get(p)) { - (Some(ver_pre), Some(ver_post)) if ver_pre != ver_post => { - Some((p, ver_pre, ver_post)) - } - _ => None, - } - }) - .for_each(|(p, ver_pre, ver_post)| { - let version_str_pre = ver_pre.iter().copied().collect::>().join(" "); - let version_str_post = ver_post.iter().copied().collect::>().join(", "); - println!( - "{} {} {} {} ~> {}", - "[C:]".bold().bright_yellow(), - p, - "@".yellow(), - version_str_pre.magenta(), - version_str_post.blue() - ); - }); + // Use sorted output for more predictable and readable results + let mut changes = Vec::new(); + + for p in set.iter().filter(|p| !p.is_empty()) { + if let (Some(ver_pre), Some(ver_post)) = (pre.get(p), post.get(p)) { + if ver_pre != ver_post { + changes.push((*p, ver_pre, ver_post)); + } + } + } + + // Sort by package name for consistent output + changes.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); + + for (p, ver_pre, ver_post) in changes { + let version_str_pre = ver_pre.iter().copied().collect::>().join(" "); + let version_str_post = ver_post.iter().copied().collect::>().join(", "); + + println!( + "{} {} {} {} ~> {}", + "[C:]".bold().bright_yellow(), + p, + "@".yellow(), + version_str_pre.magenta(), + version_str_post.blue() + ); + } }