1
Fork 0
mirror of https://github.com/RGBCube/dix synced 2025-05-18 12:05:07 +00:00

feat: refactor store.rs

This commit is contained in:
RGBCube 2025-05-08 21:54:56 +03:00 committed by bloxx12
parent 531fa0278f
commit db09147da6
14 changed files with 1168 additions and 839 deletions

7
.gitignore vendored
View file

@ -1,9 +1,2 @@
/.direnv /.direnv
/target /target
# Added by cargo
#
# already existing elements were commented out
#/target

30
.rustfmt.toml Normal file
View file

@ -0,0 +1,30 @@
# Taken from https://github.com/cull-os/carcass.
# Modified to have 2 space indents and 80 line width.
# float_literal_trailing_zero = "Always" # TODO: Warning for some reason?
condense_wildcard_suffixes = true
doc_comment_code_block_width = 80
edition = "2024" # Keep in sync with Cargo.toml.
enum_discrim_align_threshold = 60
force_explicit_abi = false
force_multiline_blocks = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
hex_literal_case = "Upper"
imports_granularity = "Crate"
imports_layout = "Vertical"
inline_attribute_width = 60
match_block_trailing_comma = true
max_width = 80
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = true
overflow_delimited_expr = true
struct_field_align_threshold = 60
tab_spaces = 2
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
wrap_comments = true

15
.taplo.toml Normal file
View file

@ -0,0 +1,15 @@
# Taken from https://github.com/cull-os/carcass.
[formatting]
align_entries = true
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false

79
Cargo.lock generated
View file

@ -61,6 +61,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -174,6 +180,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "convert_case"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.3.6" version = "0.3.6"
@ -256,6 +271,28 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "derive_more"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@ -266,14 +303,18 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
name = "dix" name = "dix"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"clap 4.5.37", "clap 4.5.37",
"criterion", "criterion",
"derive_more",
"diff", "diff",
"env_logger", "env_logger",
"libc", "libc",
"log", "log",
"ref-cast",
"regex", "regex",
"rusqlite", "rusqlite",
"rustc-hash",
"thiserror", "thiserror",
"yansi", "yansi",
] ]
@ -562,6 +603,26 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -605,6 +666,12 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.20" version = "1.0.20"
@ -742,12 +809,24 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.14" version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"

View file

@ -1,39 +1,119 @@
[package] [package]
name = "dix" name = "dix"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]]
name = "dix"
path = "src/main.rs"
[lib]
name = "dixlib"
path = "src/lib.rs"
[dependencies] [dependencies]
clap = { version = "4.5.37", features = ["derive"] } anyhow = "1.0.98"
regex = "1.11.1" clap = { version = "4.5.37", features = [ "derive" ] }
yansi = "1.0.1" derive_more = { version = "2.0.1", features = ["full"] }
thiserror = "2.0.12" diff = "0.1.13"
log = "0.4.20"
env_logger = "0.11.3" env_logger = "0.11.3"
rusqlite = { version = "0.35.0", features = ["bundled"] } log = "0.4.20"
diff = "0.1.13" ref-cast = "1.0.24"
regex = "1.11.1"
rusqlite = { version = "0.35.0", features = [ "bundled" ] }
rustc-hash = "2.1.1"
thiserror = "2.0.12"
yansi = "1.0.1"
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
libc = "0.2" libc = "0.2"
[[bench]] [[bench]]
name = "store" harness = false
harness=false name = "store"
[[bench]] [[bench]]
name = "print" harness = false
harness=false name = "print"
[[bench]] [[bench]]
name = "util" harness = false
harness=false name = "util"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
blanket_clippy_restriction_lints = "allow"
restriction = { level = "warn", priority = -1 }
alloc_instead_of_core = "allow"
allow_attributes_without_reason = "allow"
arbitrary_source_item_ordering = "allow"
arithmetic_side_effects = "allow"
as_conversions = "allow"
as_pointer_underscore = "allow"
as_underscore = "allow"
big_endian_bytes = "allow"
clone_on_ref_ptr = "allow"
dbg_macro = "allow"
disallowed_script_idents = "allow"
else_if_without_else = "allow"
error_impl_error = "allow"
exhaustive_enums = "allow"
exhaustive_structs = "allow"
expect_used = "allow"
field_scoped_visibility_modifiers = "allow"
float_arithmetic = "allow"
host_endian_bytes = "allow"
impl_trait_in_params = "allow"
implicit_return = "allow"
indexing_slicing = "allow"
inline_asm_x86_intel_syntax = "allow"
integer_division = "allow"
integer_division_remainder_used = "allow"
large_include_file = "allow"
let_underscore_must_use = "allow"
let_underscore_untyped = "allow"
little_endian_bytes = "allow"
map_err_ignore = "allow"
match_same_arms = "allow"
missing_assert_message = "allow"
missing_docs_in_private_items = "allow"
missing_errors_doc = "allow"
missing_inline_in_public_items = "allow"
missing_panics_doc = "allow"
missing_trait_methods = "allow"
mod_module_files = "allow"
multiple_inherent_impl = "allow"
mutex_atomic = "allow"
mutex_integer = "allow"
new_without_default = "allow"
non_ascii_literal = "allow"
panic = "allow"
panic_in_result_fn = "allow"
partial_pub_fields = "allow"
print_stderr = "allow"
print_stdout = "allow"
pub_use = "allow"
pub_with_shorthand = "allow"
pub_without_shorthand = "allow"
question_mark_used = "allow"
ref_patterns = "allow"
renamed_function_params = "allow"
same_name_method = "allow"
semicolon_outside_block = "allow"
separated_literal_suffix = "allow"
shadow_reuse = "allow"
shadow_same = "allow"
shadow_unrelated = "allow"
single_call_fn = "allow"
single_char_lifetime_names = "allow"
single_match_else = "allow"
std_instead_of_alloc = "allow"
std_instead_of_core = "allow"
string_add = "allow"
string_slice = "allow"
todo = "allow"
too_many_lines = "allow"
try_err = "allow"
unimplemented = "allow"
unnecessary_safety_comment = "allow"
unnecessary_safety_doc = "allow"
unreachable = "allow"
unwrap_in_result = "allow"
unwrap_used = "allow"
use_debug = "allow"
wildcard_enum_match_arm = "allow"

View file

@ -1,89 +1,94 @@
use std::{ use std::{
env, env,
fs::{self, DirEntry}, fs,
path::PathBuf, path::PathBuf,
sync::OnceLock, sync::OnceLock,
}; };
use dixlib::{store, util::PackageDiff}; use dix::{
store,
util::PackageDiff,
};
/// tries to get the path of the oldest nixos system derivation /// tries to get the path of the oldest nixos system derivation
/// this function is pretty hacky and only used so that /// this function is pretty hacky and only used so that
/// you don't have to specify a specific derivation to /// you don't have to specify a specific derivation to
/// run the benchmarks /// run the benchmarks
fn get_oldest_nixos_system() -> Option<PathBuf> { fn get_oldest_nixos_system() -> Option<PathBuf> {
let profile_dir = fs::read_dir("/nix/var/nix/profiles").ok()?; let profile_dir = fs::read_dir("/nix/var/nix/profiles").ok()?;
let files = profile_dir.filter_map(Result::ok).filter_map(|entry| { let files = profile_dir.filter_map(Result::ok).filter_map(|entry| {
entry entry
.file_type() .file_type()
.ok() .ok()
.and_then(|f| f.is_symlink().then_some(entry.path())) .and_then(|f| f.is_symlink().then_some(entry.path()))
}); });
files.min_by_key(|path| { files.min_by_key(|path| {
// extract all digits from the file name and use that as key // extract all digits from the file name and use that as key
let p = path.as_os_str().to_str().unwrap_or_default(); let p = path.as_os_str().to_str().unwrap_or_default();
let digits: String = p.chars().filter(|c| c.is_ascii_digit()).collect(); let digits: String = p.chars().filter(|c| c.is_ascii_digit()).collect();
// if we are not able to produce a key (e.g. because the path does not contain digits) // if we are not able to produce a key (e.g. because the path does not
// we put it last // contain digits) we put it last
digits.parse::<u32>().unwrap_or(u32::MAX) digits.parse::<u32>().unwrap_or(u32::MAX)
}) })
} }
pub fn get_deriv_query() -> &'static PathBuf { pub fn get_deriv_query() -> &'static PathBuf {
static _QUERY_DERIV: OnceLock<PathBuf> = OnceLock::new(); static _QUERY_DERIV: OnceLock<PathBuf> = OnceLock::new();
_QUERY_DERIV.get_or_init(|| { _QUERY_DERIV.get_or_init(|| {
let path = PathBuf::from( let path = PathBuf::from(
env::var("DIX_BENCH_NEW_SYSTEM") env::var("DIX_BENCH_NEW_SYSTEM")
.unwrap_or_else(|_| "/run/current-system/system".into()), .unwrap_or_else(|_| "/run/current-system/system".into()),
); );
path path
}) })
} }
pub fn get_deriv_query_old() -> &'static PathBuf { pub fn get_deriv_query_old() -> &'static PathBuf {
static _QUERY_DERIV: OnceLock<PathBuf> = OnceLock::new(); static _QUERY_DERIV: OnceLock<PathBuf> = OnceLock::new();
_QUERY_DERIV.get_or_init(|| { _QUERY_DERIV.get_or_init(|| {
let path = env::var("DIX_BENCH_OLD_SYSTEM") let path = env::var("DIX_BENCH_OLD_SYSTEM")
.ok() .ok()
.map(PathBuf::from) .map(PathBuf::from)
.or(get_oldest_nixos_system()) .or(get_oldest_nixos_system())
.unwrap_or_else(|| PathBuf::from("/run/current-system/system")); .unwrap_or_else(|| PathBuf::from("/run/current-system/system"));
path path
}) })
} }
pub fn get_packages() -> &'static (Vec<String>, Vec<String>) { pub fn get_packages() -> &'static (Vec<String>, Vec<String>) {
static _PKGS: OnceLock<(Vec<String>, Vec<String>)> = OnceLock::new(); static _PKGS: OnceLock<(Vec<String>, Vec<String>)> = OnceLock::new();
_PKGS.get_or_init(|| { _PKGS.get_or_init(|| {query_depdendents
let pkgs_before = store::get_packages(std::path::Path::new(get_deriv_query_old())) let pkgs_before =
.unwrap() store::query_packages(std::path::Path::new(get_deriv_query_old()))
.into_iter() .unwrap()
.map(|(_, name)| name) .into_iter()
.collect::<Vec<String>>(); .map(|(_, name)| name)query_depdendents
let pkgs_after = store::get_packages(std::path::Path::new(get_deriv_query())) .collect::<Vec<String>>();
.unwrap() let pkgs_after =
.into_iter() store::query_packages(std::path::Path::new(get_deriv_query()))
.map(|(_, name)| name) .unwrap()
.collect::<Vec<String>>(); .into_iter()
(pkgs_before, pkgs_after) .map(|(_, name)| name)
}) .collect::<Vec<String>>();
(pkgs_before, pkgs_after)
})
} }
pub fn get_pkg_diff() -> &'static PackageDiff<'static> { pub fn get_pkg_diff() -> &'static PackageDiff<'static> {
static _PKG_DIFF: OnceLock<PackageDiff> = OnceLock::new(); static _PKG_DIFF: OnceLock<PackageDiff> = OnceLock::new();
_PKG_DIFF.get_or_init(|| { _PKG_DIFF.get_or_init(|| {
let (pkgs_before, pkgs_after) = get_packages(); let (pkgs_before, pkgs_after) = get_packages();
PackageDiff::new(pkgs_before, pkgs_after) PackageDiff::new(pkgs_before, pkgs_after)
}) })
} }
/// prints the old and new NixOs system used for benchmarking /// prints the old and new NixOs system used for benchmarking
/// ///
/// is used to give information about the old and new system /// is used to give information about the old and new system
pub fn print_used_nixos_systems() { pub fn print_used_nixos_systems() {
let old = get_deriv_query_old(); let old = get_deriv_query_old();
let new = get_deriv_query(); let new = get_deriv_query();
println!("old system used {:?}", old); println!("old system used {:?}", old);
println!("new system used {:?}", new); println!("new system used {:?}", new);
} }

View file

@ -1,86 +1,97 @@
mod common; mod common;
use std::{fs::File, os::fd::AsRawFd}; use std::{
fs::File,
os::fd::AsRawFd,
};
use common::{get_pkg_diff, print_used_nixos_systems}; use common::{
use criterion::{Criterion, black_box, criterion_group, criterion_main}; get_pkg_diff,
use dixlib::print; print_used_nixos_systems,
};
use criterion::{
Criterion,
black_box,
criterion_group,
criterion_main,
};
use dix::print;
/// reroutes stdout and stderr to the null device before /// reroutes stdout and stderr to the null device before
/// executing `f` /// executing `f`
fn suppress_output<F: FnOnce()>(f: F) { fn suppress_output<F: FnOnce()>(f: F) {
let stdout = std::io::stdout(); let stdout = std::io::stdout();
let stderr = std::io::stderr(); let stderr = std::io::stderr();
// Save original FDs // Save original FDs
let orig_stdout_fd = stdout.as_raw_fd(); let orig_stdout_fd = stdout.as_raw_fd();
let orig_stderr_fd = stderr.as_raw_fd(); let orig_stderr_fd = stderr.as_raw_fd();
// Open /dev/null and get its FD // Open /dev/null and get its FD
let devnull = File::create("/dev/null").unwrap(); let devnull = File::create("/dev/null").unwrap();
let null_fd = devnull.as_raw_fd(); let null_fd = devnull.as_raw_fd();
// Redirect stdout and stderr to /dev/null // Redirect stdout and stderr to /dev/null
let _ = unsafe { libc::dup2(null_fd, orig_stdout_fd) }; let _ = unsafe { libc::dup2(null_fd, orig_stdout_fd) };
let _ = unsafe { libc::dup2(null_fd, orig_stderr_fd) }; let _ = unsafe { libc::dup2(null_fd, orig_stderr_fd) };
f(); f();
let _ = unsafe { libc::dup2(orig_stdout_fd, 1) }; let _ = unsafe { libc::dup2(orig_stdout_fd, 1) };
let _ = unsafe { libc::dup2(orig_stderr_fd, 2) }; let _ = unsafe { libc::dup2(orig_stderr_fd, 2) };
} }
pub fn bench_print_added(c: &mut Criterion) { pub fn bench_print_added(c: &mut Criterion) {
print_used_nixos_systems(); print_used_nixos_systems();
let diff = get_pkg_diff(); let diff = get_pkg_diff();
c.bench_function("print_added", |b| { c.bench_function("print_added", |b| {
b.iter(|| { b.iter(|| {
suppress_output(|| { suppress_output(|| {
print::print_added( print::print_added(
black_box(&diff.added), black_box(&diff.added),
black_box(&diff.pkg_to_versions_post), black_box(&diff.pkg_to_versions_post),
30, 30,
); );
}); });
});
}); });
});
} }
pub fn bench_print_removed(c: &mut Criterion) { pub fn bench_print_removed(c: &mut Criterion) {
print_used_nixos_systems(); print_used_nixos_systems();
let diff = get_pkg_diff(); let diff = get_pkg_diff();
c.bench_function("print_removed", |b| { c.bench_function("print_removed", |b| {
b.iter(|| { b.iter(|| {
suppress_output(|| { suppress_output(|| {
print::print_removed( print::print_removed(
black_box(&diff.removed), black_box(&diff.removed),
black_box(&diff.pkg_to_versions_pre), black_box(&diff.pkg_to_versions_pre),
30, 30,
); );
}); });
});
}); });
});
} }
pub fn bench_print_changed(c: &mut Criterion) { pub fn bench_print_changed(c: &mut Criterion) {
print_used_nixos_systems(); print_used_nixos_systems();
let diff = get_pkg_diff(); let diff = get_pkg_diff();
c.bench_function("print_changed", |b| { c.bench_function("print_changed", |b| {
b.iter(|| { b.iter(|| {
suppress_output(|| { suppress_output(|| {
print::print_changes( print::print_changes(
black_box(&diff.changed), black_box(&diff.changed),
black_box(&diff.pkg_to_versions_pre), black_box(&diff.pkg_to_versions_pre),
black_box(&diff.pkg_to_versions_post), black_box(&diff.pkg_to_versions_post),
30, 30,
); );
}); });
});
}); });
});
} }
criterion_group!( criterion_group!(
benches, benches,
bench_print_added, bench_print_added,
bench_print_removed, bench_print_removed,
bench_print_changed bench_print_changed
); );
criterion_main!(benches); criterion_main!(benches);

View file

@ -1,6 +1,11 @@
mod common; mod common;
use criterion::{Criterion, black_box, criterion_group, criterion_main}; use criterion::{
use dixlib::store; Criterion,
black_box,
criterion_group,
criterion_main,
};
use dix::store;
// basic benchmarks using the current system // basic benchmarks using the current system
// //
@ -12,25 +17,27 @@ use dixlib::store;
// db to benchmark instead to make the results comparable // db to benchmark instead to make the results comparable
pub fn bench_get_packages(c: &mut Criterion) { pub fn bench_get_packages(c: &mut Criterion) {
c.bench_function("get_packages", |b| { c.bench_function("get_packages", |b| {
b.iter(|| store::get_packages(black_box(common::get_deriv_query()))); b.iter(|| store::query_depdendents(black_box(common::get_deriv_query())));
}); });
} }
pub fn bench_get_closure_size(c: &mut Criterion) { pub fn bench_get_closure_size(c: &mut Criterion) {
c.bench_function("get_closure_size", |b| { c.bench_function("get_closure_size", |b| {
b.iter(|| store::get_closure_size(black_box(common::get_deriv_query()))); b.iter(|| store::gequery_closure_sizelack_box(common::get_deriv_query())));
}); });
} }
pub fn bench_get_dependency_graph(c: &mut Criterion) { pub fn bench_get_dependency_graph(c: &mut Criterion) {
c.bench_function("get_dependency_graph", |b| { c.bench_function("get_dependency_graph", |b| {
b.iter(|| store::get_dependency_graph(black_box(common::get_deriv_query()))); b.iter(|| {
store::query_dependency_graph(black_box(common::get_deriv_query()))
}); });
});
} }
criterion_group!( criterion_group!(
benches, benches,
bench_get_packages, bench_get_packages,
bench_get_closure_size, bench_get_closure_size,
bench_get_dependency_graph bench_get_dependency_graph
); );
criterion_main!(benches); criterion_main!(benches);

View file

@ -1,14 +1,19 @@
mod common; mod common;
use common::get_packages; use common::get_packages;
use criterion::{Criterion, black_box, criterion_group, criterion_main}; use criterion::{
use dixlib::util::PackageDiff; Criterion,
black_box,
criterion_group,
criterion_main,
};
use dix::util::PackageDiff;
pub fn bench_package_diff(c: &mut Criterion) { pub fn bench_package_diff(c: &mut Criterion) {
let (pkgs_before, pkgs_after) = get_packages(); let (pkgs_before, pkgs_after) = get_packages();
c.bench_function("PackageDiff::new", |b| { c.bench_function("PackageDiff::new", |b| {
b.iter(|| PackageDiff::new(black_box(pkgs_before), black_box(pkgs_after))); b.iter(|| PackageDiff::new(black_box(pkgs_before), black_box(pkgs_after)));
}); });
} }
criterion_group!(benches, bench_package_diff); criterion_group!(benches, bench_package_diff);

View file

@ -3,121 +3,131 @@ use thiserror::Error;
/// Application errors with thiserror /// Application errors with thiserror
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum AppError { pub enum AppError {
#[error("Command failed: {command} {args:?} - {message}")] #[error("Command failed: {command} {args:?} - {message}")]
CommandFailed { CommandFailed {
command: String, command: String,
args: Vec<String>, args: Vec<String>,
message: String, message: String,
}, },
#[error("Failed to decode command output from {context}: {source}")] #[error("Failed to decode command output from {context}: {source}")]
CommandOutputError { CommandOutputError {
source: std::str::Utf8Error, source: std::str::Utf8Error,
context: String, context: String,
}, },
#[error("Failed to parse data in {context}: {message}")] #[error("Failed to parse data in {context}: {message}")]
ParseError { ParseError {
message: String, message: String,
context: String, context: String,
#[source] #[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>, source: Option<Box<dyn std::error::Error + Send + Sync>>,
}, },
#[error("Regex error in {context}: {source}")] #[error("Regex error in {context}: {source}")]
RegexError { RegexError {
source: regex::Error, source: regex::Error,
context: String, context: String,
}, },
#[error("IO error in {context}: {source}")] #[error("IO error in {context}: {source}")]
IoError { IoError {
source: std::io::Error, source: std::io::Error,
context: String, context: String,
}, },
#[error("Database error: {source}")] #[error("Database error: {source}")]
DatabaseError { source: rusqlite::Error }, DatabaseError { source: rusqlite::Error },
} }
// Implement From traits to support the ? operator // Implement From traits to support the ? operator
impl From<std::io::Error> for AppError { impl From<std::io::Error> for AppError {
fn from(source: std::io::Error) -> Self { fn from(source: std::io::Error) -> Self {
Self::IoError { Self::IoError {
source, source,
context: "unknown context".into(), context: "unknown context".into(),
}
} }
}
} }
impl From<std::str::Utf8Error> for AppError { impl From<std::str::Utf8Error> for AppError {
fn from(source: std::str::Utf8Error) -> Self { fn from(source: std::str::Utf8Error) -> Self {
Self::CommandOutputError { Self::CommandOutputError {
source, source,
context: "command output".into(), context: "command output".into(),
}
} }
}
} }
impl From<rusqlite::Error> for AppError { impl From<rusqlite::Error> for AppError {
fn from(source: rusqlite::Error) -> Self { fn from(source: rusqlite::Error) -> Self {
Self::DatabaseError { source } Self::DatabaseError { source }
} }
} }
impl From<regex::Error> for AppError { impl From<regex::Error> for AppError {
fn from(source: regex::Error) -> Self { fn from(source: regex::Error) -> Self {
Self::RegexError { Self::RegexError {
source, source,
context: "regex operation".into(), context: "regex operation".into(),
}
} }
}
} }
impl AppError { impl AppError {
/// Create a command failure error with context /// Create a command failure error with context
pub fn command_failed<S: Into<String>>(command: S, args: &[&str], message: S) -> Self { pub fn command_failed<S: Into<String>>(
Self::CommandFailed { command: S,
command: command.into(), args: &[&str],
args: args.iter().map(|&s| s.to_string()).collect(), message: S,
message: message.into(), ) -> Self {
} Self::CommandFailed {
command: command.into(),
args: args.iter().map(|&s| s.to_string()).collect(),
message: message.into(),
} }
}
/// Create a parse error with context /// Create a parse error with context
pub fn parse_error<S: Into<String>, C: Into<String>>( pub fn parse_error<S: Into<String>, C: Into<String>>(
message: S, message: S,
context: C, context: C,
source: Option<Box<dyn std::error::Error + Send + Sync>>, source: Option<Box<dyn std::error::Error + Send + Sync>>,
) -> Self { ) -> Self {
Self::ParseError { Self::ParseError {
message: message.into(), message: message.into(),
context: context.into(), context: context.into(),
source, source,
}
} }
}
/// Create an IO error with context /// Create an IO error with context
pub fn io_error<C: Into<String>>(source: std::io::Error, context: C) -> Self { pub fn io_error<C: Into<String>>(source: std::io::Error, context: C) -> Self {
Self::IoError { Self::IoError {
source, source,
context: context.into(), context: context.into(),
}
} }
}
/// Create a regex error with context /// Create a regex error with context
pub fn regex_error<C: Into<String>>(source: regex::Error, context: C) -> Self { pub fn regex_error<C: Into<String>>(
Self::RegexError { source: regex::Error,
source, context: C,
context: context.into(), ) -> Self {
} Self::RegexError {
source,
context: context.into(),
} }
}
/// Create a command output error with context /// Create a command output error with context
pub fn command_output_error<C: Into<String>>(source: std::str::Utf8Error, context: C) -> Self { pub fn command_output_error<C: Into<String>>(
Self::CommandOutputError { source: std::str::Utf8Error,
source, context: C,
context: context.into(), ) -> Self {
} Self::CommandOutputError {
source,
context: context.into(),
} }
}
} }

View file

@ -1,12 +1,18 @@
use clap::Parser;
use core::str; use core::str;
use dixlib::print;
use dixlib::store;
use dixlib::util::PackageDiff;
use log::{debug, error};
use std::{ use std::{
collections::{HashMap, HashSet}, collections::HashSet,
thread, thread,
};
use clap::Parser;
use dixlib::{
print,
store,
util::PackageDiff,
};
use log::{
debug,
error,
}; };
use yansi::Paint; use yansi::Paint;
@ -16,199 +22,204 @@ use yansi::Paint;
#[command(about = "Diff Nix stuff", long_about = None)] #[command(about = "Diff Nix stuff", long_about = None)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Args { struct Args {
path: std::path::PathBuf, path: std::path::PathBuf,
path2: std::path::PathBuf, path2: std::path::PathBuf,
/// Print the whole store paths /// Print the whole store paths
#[arg(short, long)] #[arg(short, long)]
paths: bool, paths: bool,
/// Print the closure size /// Print the closure size
#[arg(long, short)] #[arg(long, short)]
closure_size: bool, closure_size: bool,
/// Verbosity level: -v for debug, -vv for trace /// Verbosity level: -v for debug, -vv for trace
#[arg(short, long, action = clap::ArgAction::Count)] #[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8, verbose: u8,
/// Silence all output except errors /// Silence all output except errors
#[arg(short, long)] #[arg(short, long)]
quiet: bool, quiet: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Package<'a> { struct Package<'a> {
name: &'a str, name: &'a str,
versions: HashSet<&'a str>, versions: HashSet<&'a str>,
/// Save if a package is a dependency of another package /// Save if a package is a dependency of another package
is_dep: bool, is_dep: bool,
} }
impl<'a> Package<'a> { impl<'a> Package<'a> {
fn new(name: &'a str, version: &'a str, is_dep: bool) -> Self { fn new(name: &'a str, version: &'a str, is_dep: bool) -> Self {
let mut versions = HashSet::new(); let mut versions = HashSet::new();
versions.insert(version); versions.insert(version);
Self { Self {
name, name,
versions, versions,
is_dep, is_dep,
}
} }
}
fn add_version(&mut self, version: &'a str) { fn add_version(&mut self, version: &'a str) {
self.versions.insert(version); self.versions.insert(version);
} }
} }
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)] #[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
// Configure logger based on verbosity flags and environment variables // Configure logger based on verbosity flags and environment variables
// Respects RUST_LOG environment variable if present. // Respects RUST_LOG environment variable if present.
// XXX:We can also dedicate a specific env variable for this tool, if we want to. // XXX:We can also dedicate a specific env variable for this tool, if we want
let env = env_logger::Env::default().filter_or( // to.
"RUST_LOG", let env = env_logger::Env::default().filter_or(
if args.quiet { "RUST_LOG",
"error" if args.quiet {
} else { "error"
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();
// 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((
thread::spawn(move || store::get_closure_size(&path)),
thread::spawn(move || store::get_closure_size(&path2)),
))
} else { } else {
None match args.verbose {
}; 0 => "info",
1 => "debug",
_ => "trace",
}
},
);
// Get package lists and handle potential errors // Build and initialize the logger
let package_list_pre = match store::get_packages(&args.path) { env_logger::Builder::from_env(env)
Ok(packages) => { .format_timestamp(Some(env_logger::fmt::TimestampPrecision::Seconds))
debug!("Found {} packages in first closure", packages.len()); .init();
packages.into_iter().map(|(_, path)| path).collect()
}
Err(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 store::get_packages(&args.path2) { // handles to the threads collecting closure size information
Ok(packages) => { // We do this as early as possible because nix is slow.
debug!("Found {} packages in second closure", packages.len()); let closure_size_handles = if args.closure_size {
packages.into_iter().map(|(_, path)| path).collect() debug!("Calculating closure sizes in background");
} let path = args.path.clone();
Err(e) => { let path2 = args.path2.clone();
error!( Some((
"Error getting packages from path {}: {}", thread::spawn(move || store::get_closure_size(&path)),
args.path2.display(), thread::spawn(move || store::get_closure_size(&path2)),
e ))
); } else {
eprintln!( None
"Error getting packages from path {}: {}", };
args.path2.display(),
e
);
Vec::new()
}
};
let PackageDiff { // Get package lists and handle potential errors
pkg_to_versions_pre: pre, let package_list_pre = match store::query_packages(&args.path) {
pkg_to_versions_post: post, Ok(packages) => {
pre_keys: _, debug!("Found {} packages in first closure", packages.len());
post_keys: _, packages.into_iter().map(|(_, path)| path).collect()
added, },
removed, Err(e) => {
changed, error!(
} = PackageDiff::new(&package_list_pre, &package_list_post); "Error getting packages from path {}: {}",
args.path.display(),
e
);
eprintln!(
"Error getting packages from path {}: {}",
args.path.display(),
e
);
Vec::new()
},
};
debug!("Added packages: {}", added.len()); let package_list_post = match store::query_packages(&args.path2) {
debug!("Removed packages: {}", removed.len()); Ok(packages) => {
debug!( debug!("Found {} packages in second closure", packages.len());
"Changed packages: {}", packages.into_iter().map(|(_, path)| path).collect()
changed },
.iter() Err(e) => {
.filter(|p| !p.is_empty() error!(
&& match (pre.get(*p), post.get(*p)) { "Error getting packages from path {}: {}",
(Some(ver_pre), Some(ver_post)) => ver_pre != ver_post, args.path2.display(),
_ => false, e
}) );
.count() eprintln!(
); "Error getting packages from path {}: {}",
args.path2.display(),
e
);
Vec::new()
},
};
println!("Difference between the two generations:"); let PackageDiff {
println!(); pkg_to_versions_pre: pre,
pkg_to_versions_post: post,
pre_keys: _,
post_keys: _,
added,
removed,
changed,
} = PackageDiff::new(&package_list_pre, &package_list_post);
let width_changes = changed debug!("Added packages: {}", added.len());
.iter() debug!("Removed packages: {}", removed.len());
.filter(|&&p| match (pre.get(p), post.get(p)) { debug!(
(Some(version_pre), Some(version_post)) => version_pre != version_post, "Changed packages: {}",
changed
.iter()
.filter(|p| {
!p.is_empty()
&& match (pre.get(*p), post.get(*p)) {
(Some(ver_pre), Some(ver_post)) => ver_pre != ver_post,
_ => false, _ => false,
}); }
})
.count()
);
let col_width = added println!("Difference between the two generations:");
.iter() println!();
.chain(removed.iter())
.chain(width_changes)
.map(|p| p.len())
.max()
.unwrap_or_default();
println!("<<< {}", args.path.to_string_lossy()); let width_changes = changed.iter().filter(|&&p| {
println!(">>> {}", args.path2.to_string_lossy()); match (pre.get(p), post.get(p)) {
print::print_added(&added, &post, col_width); (Some(version_pre), Some(version_post)) => version_pre != version_post,
print::print_removed(&removed, &pre, col_width); _ => false,
print::print_changes(&changed, &pre, &post, col_width);
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))) => {
let pre_size = pre_size / 1024 / 1024;
let post_size = post_size / 1024 / 1024;
debug!("Pre closure size: {pre_size} MiB");
debug!("Post closure size: {post_size} MiB");
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");
}
}
} }
});
let col_width = added
.iter()
.chain(removed.iter())
.chain(width_changes)
.map(|p| p.len())
.max()
.unwrap_or_default();
println!("<<< {}", args.path.to_string_lossy());
println!(">>> {}", args.path2.to_string_lossy());
print::print_added(&added, &post, col_width);
print::print_removed(&removed, &pre, col_width);
print::print_changes(&changed, &pre, &post, col_width);
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))) => {
let pre_size = pre_size / 1024 / 1024;
let post_size = post_size / 1024 / 1024;
debug!("Pre closure size: {pre_size} MiB");
debug!("Post closure size: {post_size} MiB");
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"
);
},
}
}
} }

View file

@ -1,10 +1,14 @@
use core::str; use core::str;
use regex::Regex;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{
string::ToString, HashMap,
sync::OnceLock, HashSet,
},
string::ToString,
sync::OnceLock,
}; };
use regex::Regex;
use yansi::Paint; use yansi::Paint;
/// diffs two strings character by character, and returns a tuple of strings /// diffs two strings character by character, and returns a tuple of strings
@ -12,179 +16,191 @@ use yansi::Paint;
/// ///
/// # Returns: /// # Returns:
/// ///
/// * (String, String) - The differing chars being red in the left, and green in the right one. /// * (String, String) - The differing chars being red in the left, and green in
/// the right one.
fn diff_versions(left: &str, right: &str) -> (String, String) { fn diff_versions(left: &str, right: &str) -> (String, String) {
let mut prev = "\x1b[33m".to_string(); let mut prev = "\x1b[33m".to_string();
let mut post = "\x1b[33m".to_string(); let mut post = "\x1b[33m".to_string();
// We only have to filter the left once, since we stop if the left one is empty. // We only have to filter the left once, since we stop if the left one is
// We do this to display things like -man, -dev properly. // empty. We do this to display things like -man, -dev properly.
let matches = name_regex().captures(left); let matches = name_regex().captures(left);
let mut suffix = String::new(); let mut suffix = String::new();
if let Some(m) = matches { if let Some(m) = matches {
let tmp = m.get(0).map_or("", |m| m.as_str()); let tmp = m.get(0).map_or("", |m| m.as_str());
suffix.push_str(tmp); suffix.push_str(tmp);
}
// string without the suffix
let filtered_left = &left[..left.len() - suffix.len()];
let filtered_right = &right[..right.len() - suffix.len()];
for diff in diff::chars(filtered_left, filtered_right) {
match diff {
diff::Result::Both(l, _) => {
let string_to_push = format!("{l}");
prev.push_str(&string_to_push);
post.push_str(&string_to_push);
},
diff::Result::Left(l) => {
let string_to_push = format!("\x1b[1;91m{l}");
prev.push_str(&string_to_push);
},
diff::Result::Right(r) => {
let string_to_push = format!("\x1b[1;92m{r}");
post.push_str(&string_to_push);
},
} }
// string without the suffix }
let filtered_left = &left[..left.len() - suffix.len()];
let filtered_right = &right[..right.len() - suffix.len()];
for diff in diff::chars(filtered_left, filtered_right) { // push removed suffix
match diff { prev.push_str(&format!("\x1b[33m{}", &suffix));
diff::Result::Both(l, _) => { post.push_str(&format!("\x1b[33m{}", &suffix));
let string_to_push = format!("{l}");
prev.push_str(&string_to_push);
post.push_str(&string_to_push);
}
diff::Result::Left(l) => {
let string_to_push = format!("\x1b[1;91m{l}");
prev.push_str(&string_to_push);
}
diff::Result::Right(r) => { // reset
let string_to_push = format!("\x1b[1;92m{r}"); prev.push_str("\x1b[0m");
post.push_str(&string_to_push); post.push_str("\x1b[0m");
}
}
}
// push removed suffix (prev, post)
prev.push_str(&format!("\x1b[33m{}", &suffix));
post.push_str(&format!("\x1b[33m{}", &suffix));
//reset
prev.push_str("\x1b[0m");
post.push_str("\x1b[0m");
(prev, post)
} }
/// print the packages added between two closures. /// print the packages added between two closures.
pub fn print_added(set: &HashSet<&str>, post: &HashMap<&str, HashSet<&str>>, col_width: usize) { pub fn print_added(
println!("{}", "Packages added:".underline().bold()); set: &HashSet<&str>,
post: &HashMap<&str, HashSet<&str>>,
col_width: usize,
) {
println!("{}", "Packages added:".underline().bold());
// Use sorted outpu // Use sorted outpu
let mut sorted: Vec<_> = set let mut sorted: Vec<_> = set
.iter() .iter()
.filter_map(|p| post.get(p).map(|ver| (*p, ver))) .filter_map(|p| post.get(p).map(|ver| (*p, ver)))
.collect(); .collect();
// Sort by package name for consistent output // Sort by package name for consistent output
sorted.sort_by(|(a, _), (b, _)| a.cmp(b)); sorted.sort_by(|(a, _), (b, _)| a.cmp(b));
for (p, ver) in sorted { for (p, ver) in sorted {
let mut version_vec = ver.iter().copied().collect::<Vec<_>>(); let mut version_vec = ver.iter().copied().collect::<Vec<_>>();
version_vec.sort_unstable(); version_vec.sort_unstable();
let version_str = version_vec.join(", "); let version_str = version_vec.join(", ");
println!( println!(
"[{}] {:col_width$} \x1b[33m{}\x1b[0m", "[{}] {:col_width$} \x1b[33m{}\x1b[0m",
"A:".green().bold(), "A:".green().bold(),
p, p,
version_str version_str
); );
} }
} }
/// print the packages removed between two closures. /// print the packages removed between two closures.
pub fn print_removed(set: &HashSet<&str>, pre: &HashMap<&str, HashSet<&str>>, col_width: usize) { pub fn print_removed(
println!("{}", "Packages removed:".underline().bold()); set: &HashSet<&str>,
pre: &HashMap<&str, HashSet<&str>>,
col_width: usize,
) {
println!("{}", "Packages removed:".underline().bold());
// Use sorted output for more predictable and readable results // Use sorted output for more predictable and readable results
let mut sorted: Vec<_> = set let mut sorted: Vec<_> = set
.iter() .iter()
.filter_map(|p| pre.get(p).map(|ver| (*p, ver))) .filter_map(|p| pre.get(p).map(|ver| (*p, ver)))
.collect(); .collect();
// Sort by package name for consistent output // Sort by package name for consistent output
sorted.sort_by(|(a, _), (b, _)| a.cmp(b)); sorted.sort_by(|(a, _), (b, _)| a.cmp(b));
for (p, ver) in sorted { for (p, ver) in sorted {
let mut version_vec = ver.iter().copied().collect::<Vec<_>>(); let mut version_vec = ver.iter().copied().collect::<Vec<_>>();
version_vec.sort_unstable(); version_vec.sort_unstable();
let version_str = version_vec.join(", "); let version_str = version_vec.join(", ");
println!( println!(
"[{}] {:col_width$} \x1b[33m{}\x1b[0m", "[{}] {:col_width$} \x1b[33m{}\x1b[0m",
"R:".red().bold(), "R:".red().bold(),
p, p,
version_str version_str
); );
} }
} }
pub fn print_changes( pub fn print_changes(
set: &HashSet<&str>, set: &HashSet<&str>,
pre: &HashMap<&str, HashSet<&str>>, pre: &HashMap<&str, HashSet<&str>>,
post: &HashMap<&str, HashSet<&str>>, post: &HashMap<&str, HashSet<&str>>,
col_width: usize, col_width: usize,
) { ) {
println!("{}", "Version changes:".underline().bold()); println!("{}", "Versions changed:".underline().bold());
// Use sorted output for more predictable and readable results // Use sorted output for more predictable and readable results
let mut changes = Vec::new(); let mut changes = Vec::new();
for p in set.iter().filter(|p| !p.is_empty()) { 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 let (Some(ver_pre), Some(ver_post)) = (pre.get(p), post.get(p)) {
if ver_pre != ver_post { if ver_pre != ver_post {
changes.push((*p, 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 mut version_vec_pre =
ver_pre.difference(ver_post).copied().collect::<Vec<_>>();
let mut version_vec_post =
ver_post.difference(ver_pre).copied().collect::<Vec<_>>();
version_vec_pre.sort_unstable();
version_vec_post.sort_unstable();
let mut diffed_pre: String;
let diffed_post: String;
if version_vec_pre.len() == version_vec_post.len() {
let mut diff_pre: Vec<String> = vec![];
let mut diff_post: Vec<String> = vec![];
for (pre, post) in version_vec_pre.iter().zip(version_vec_post.iter()) {
let (a, b) = diff_versions(pre, post);
diff_pre.push(a);
diff_post.push(b);
}
diffed_pre = diff_pre.join(", ");
diffed_post = diff_post.join(", ");
} else {
let version_str_pre = version_vec_pre.join(", ");
let version_str_post = version_vec_post.join(", ");
(diffed_pre, diffed_post) =
diff_versions(&version_str_pre, &version_str_post);
} }
// Sort by package name for consistent output // push a space to the diffed_pre, if it is non-empty, we do this here and
changes.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); // not in the println in order to properly align the ±.
if !version_vec_pre.is_empty() {
for (p, ver_pre, ver_post) in changes { let mut tmp = " ".to_string();
let mut version_vec_pre = ver_pre.difference(ver_post).copied().collect::<Vec<_>>(); tmp.push_str(&diffed_pre);
let mut version_vec_post = ver_post.difference(ver_pre).copied().collect::<Vec<_>>(); diffed_pre = tmp;
version_vec_pre.sort_unstable();
version_vec_post.sort_unstable();
let mut diffed_pre: String;
let diffed_post: String;
if version_vec_pre.len() == version_vec_post.len() {
let mut diff_pre: Vec<String> = vec![];
let mut diff_post: Vec<String> = vec![];
for (pre, post) in version_vec_pre.iter().zip(version_vec_post.iter()) {
let (a, b) = diff_versions(pre, post);
diff_pre.push(a);
diff_post.push(b);
}
diffed_pre = diff_pre.join(", ");
diffed_post = diff_post.join(", ");
} else {
let version_str_pre = version_vec_pre.join(", ");
let version_str_post = version_vec_post.join(", ");
(diffed_pre, diffed_post) = diff_versions(&version_str_pre, &version_str_post);
}
// push a space to the diffed_pre, if it is non-empty, we do this here and not in the println
// in order to properly align the ±.
if !version_vec_pre.is_empty() {
let mut tmp = " ".to_string();
tmp.push_str(&diffed_pre);
diffed_pre = tmp;
}
println!(
"[{}] {:col_width$}{} \x1b[0m\u{00B1}\x1b[0m {}",
"C:".bold().bright_yellow(),
p,
diffed_pre,
diffed_post
);
} }
println!(
"[{}] {:col_width$}{} \x1b[0m\u{00B1}\x1b[0m {}",
"C:".bold().bright_yellow(),
p,
diffed_pre,
diffed_post
);
}
} }
// Returns a reference to the compiled regex pattern. // Returns a reference to the compiled regex pattern.
// The regex is compiled only once. // The regex is compiled only once.
fn name_regex() -> &'static Regex { fn name_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new(); static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| { REGEX.get_or_init(|| {
Regex::new(r"(-man|-lib|-doc|-dev|-out|-terminfo)") Regex::new(r"(-man|-lib|-doc|-dev|-out|-terminfo)")
.expect("Failed to compile regex pattern for name") .expect("Failed to compile regex pattern for name")
}) })
} }

View file

@ -1,115 +1,167 @@
use std::collections::HashMap; use std::{
path::{
Path,
PathBuf,
},
result,
};
use crate::error::AppError; use anyhow::{
Context as _,
Result,
};
use derive_more::Deref;
use ref_cast::RefCast;
use rusqlite::Connection; use rusqlite::Connection;
use rustc_hash::{
FxBuildHasher,
FxHashMap,
};
// Use type alias for Result with our custom error type macro_rules! path_to_str {
type Result<T> = std::result::Result<T, AppError>; ($path:ident) => {
let $path = $path.canonicalize().with_context(|| {
format!(
"failed to canonicalize path '{path}'",
path = $path.display(),
)
})?;
const DATABASE_URL: &str = "/nix/var/nix/db/db.sqlite"; let $path = $path.to_str().with_context(|| {
format!(
const QUERY_PKGS: &str = " "failed to convert path '{path}' to valid unicode",
WITH RECURSIVE path = $path.display(),
graph(p) AS ( )
SELECT id })?;
FROM ValidPaths };
WHERE path = ?
UNION
SELECT reference FROM Refs
JOIN graph ON referrer = p
)
SELECT id, path from graph
JOIN ValidPaths ON id = p;
";
const QUERY_CLOSURE_SIZE: &str = "
WITH RECURSIVE
graph(p) AS (
SELECT id
FROM ValidPaths
WHERE path = ?
UNION
SELECT reference FROM Refs
JOIN graph ON referrer = p
)
SELECT SUM(narSize) as sum from graph
JOIN ValidPaths ON p = id;
";
const QUERY_DEPENDENCY_GRAPH: &str = "
WITH RECURSIVE
graph(p, c) AS (
SELECT id as par, reference as chd
FROM ValidPaths
JOIN Refs ON referrer = id
WHERE path = ?
UNION
SELECT referrer as par, reference as chd FROM Refs
JOIN graph ON referrer = c
)
SELECT p, c from graph;
";
/// executes a query on the nix db directly
/// to gather all derivations that the derivation given by the path
/// depends on
///
/// The ids of the derivations in the database are returned as well, since these
/// can be used to later convert nodes (represented by the the ids) of the
/// dependency graph to actual paths
///
/// in the future, we might wan't to switch to async
pub fn get_packages(path: &std::path::Path) -> Result<Vec<(i64, String)>> {
// resolve symlinks and convert to a string
let p: String = path.canonicalize()?.to_string_lossy().into_owned();
let conn = Connection::open(DATABASE_URL)?;
let mut stmt = conn.prepare_cached(QUERY_PKGS)?;
let queried_pkgs: std::result::Result<Vec<(i64, String)>, _> = stmt
.query_map([p], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect();
Ok(queried_pkgs?)
} }
/// executes a query on the nix db directly #[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// to get the total closure size of the derivation pub struct DerivationId(i64);
/// by summing up the nar size of all derivations
/// depending on the derivation
///
/// in the future, we might wan't to switch to async
pub fn get_closure_size(path: &std::path::Path) -> Result<i64> {
// resolve symlinks and convert to a string
let p: String = path.canonicalize()?.to_string_lossy().into_owned();
let conn = Connection::open(DATABASE_URL)?;
let mut stmt = conn.prepare_cached(QUERY_CLOSURE_SIZE)?; #[expect(clippy::module_name_repetitions)]
let queried_sum = stmt.query_row([p], |row| row.get(0))?; #[derive(RefCast, Deref, Debug, PartialEq, Eq)]
Ok(queried_sum) #[repr(transparent)]
pub struct StorePath(Path);
#[expect(clippy::module_name_repetitions)]
#[derive(Deref, Debug, Clone, PartialEq, Eq)]
pub struct StorePathBuf(PathBuf);
/// Connects to the Nix database.
pub fn connect() -> Result<Connection> {
const DATABASE_PATH: &str = "/nix/var/nix/db/db.sqlite";
Connection::open(DATABASE_PATH).with_context(|| {
format!("failed to connect to Nix database at {DATABASE_PATH}")
})
} }
/// returns the complete dependency graph of /// Gathers all derivations that the given store path depends on.
/// of the derivation as an adjacency list. The nodes are pub fn query_depdendents(
/// represented by the DB ids connection: &mut Connection,
path: &StorePath,
) -> Result<Vec<(DerivationId, StorePathBuf)>> {
const QUERY: &str = "
WITH RECURSIVE
graph(p) AS (
SELECT id
FROM ValidPaths
WHERE path = ?
UNION
SELECT reference FROM Refs
JOIN graph ON referrer = p
)
SELECT id, path from graph
JOIN ValidPaths ON id = p;
";
path_to_str!(path);
let packages: result::Result<Vec<(DerivationId, StorePathBuf)>, _> =
connection
.prepare_cached(QUERY)?
.query_map([path], |row| {
Ok((
DerivationId(row.get(0)?),
StorePathBuf(row.get::<_, String>(1)?.into()),
))
})?
.collect();
Ok(packages?)
}
/// Gets the total closure size of the given store path by summing up the nar
/// size of all depdendent derivations.
pub fn query_closure_size(
connection: &mut Connection,
path: &StorePath,
) -> Result<usize> {
const QUERY: &str = "
WITH RECURSIVE
graph(p) AS (
SELECT id
FROM ValidPaths
WHERE path = ?
UNION
SELECT reference FROM Refs
JOIN graph ON referrer = p
)
SELECT SUM(narSize) as sum from graph
JOIN ValidPaths ON p = id;
";
path_to_str!(path);
let closure_size = connection
.prepare_cached(QUERY)?
.query_row([path], |row| row.get(0))?;
Ok(closure_size)
}
/// Gathers the complete dependency graph of of the store path as an adjacency
/// list.
/// ///
/// We might want to collect the paths in the graph directly as /// We might want to collect the paths in the graph directly as
/// well in the future, depending on how much we use them /// well in the future, depending on how much we use them
/// in the operations on the graph /// in the operations on the graph.
/// pub fn query_dependency_graph(
/// The mapping from id to graph can be obtained by using [``get_packages``] connection: &mut Connection,
pub fn get_dependency_graph(path: &std::path::Path) -> Result<HashMap<i64, Vec<i64>>> { path: &StorePath,
// resolve symlinks and convert to a string ) -> Result<FxHashMap<DerivationId, Vec<DerivationId>>> {
let p: String = path.canonicalize()?.to_string_lossy().into_owned(); const QUERY: &str = "
let conn = Connection::open(DATABASE_URL)?; WITH RECURSIVE
graph(p, c) AS (
SELECT id as par, reference as chd
FROM ValidPaths
JOIN Refs ON referrer = id
WHERE path = ?
UNION
SELECT referrer as par, reference as chd FROM Refs
JOIN graph ON referrer = c
)
SELECT p, c from graph;
";
let mut stmt = conn.prepare_cached(QUERY_DEPENDENCY_GRAPH)?; path_to_str!(path);
let mut adj = HashMap::<i64, Vec<i64>>::new();
let queried_edges =
stmt.query_map([p], |row| Ok::<(i64, i64), _>((row.get(0)?, row.get(1)?)))?;
for row in queried_edges {
let (from, to) = row?;
adj.entry(from).or_default().push(to);
adj.entry(to).or_default();
}
Ok(adj) let mut adj =
FxHashMap::<DerivationId, Vec<DerivationId>>::with_hasher(FxBuildHasher);
let mut statement = connection.prepare_cached(QUERY)?;
let edges = statement.query_map([path], |row| {
Ok((DerivationId(row.get(0)?), DerivationId(row.get(1)?)))
})?;
for row in edges {
let (from, to) = row?;
adj.entry(from).or_default().push(to);
adj.entry(to).or_default();
}
Ok(adj)
} }

View file

@ -1,13 +1,17 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet}, collections::{
sync::OnceLock, HashMap,
HashSet,
},
sync::OnceLock,
}; };
use crate::error::AppError;
use log::debug; use log::debug;
use regex::Regex; use regex::Regex;
use crate::error::AppError;
// Use type alias for Result with our custom error type // Use type alias for Result with our custom error type
type Result<T> = std::result::Result<T, AppError>; type Result<T> = std::result::Result<T, AppError>;
@ -15,81 +19,87 @@ use std::string::ToString;
#[derive(Eq, PartialEq, Debug)] #[derive(Eq, PartialEq, Debug)]
enum VersionComponent { enum VersionComponent {
Number(u64), Number(u64),
Text(String), Text(String),
} }
impl std::cmp::Ord for VersionComponent { impl std::cmp::Ord for VersionComponent {
fn cmp(&self, other: &Self) -> Ordering { fn cmp(&self, other: &Self) -> Ordering {
use VersionComponent::{Number, Text}; use VersionComponent::{
match (self, other) { Number,
(Number(x), Number(y)) => x.cmp(y), Text,
(Text(x), Text(y)) => match (x.as_str(), y.as_str()) { };
("pre", _) => Ordering::Less, match (self, other) {
(_, "pre") => Ordering::Greater, (Number(x), Number(y)) => x.cmp(y),
_ => x.cmp(y), (Text(x), Text(y)) => {
}, match (x.as_str(), y.as_str()) {
(Text(_), Number(_)) => Ordering::Less, ("pre", _) => Ordering::Less,
(Number(_), Text(_)) => Ordering::Greater, (_, "pre") => Ordering::Greater,
_ => x.cmp(y),
} }
},
(Text(_), Number(_)) => Ordering::Less,
(Number(_), Text(_)) => Ordering::Greater,
} }
}
} }
impl PartialOrd for VersionComponent { impl PartialOrd for VersionComponent {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))
} }
} }
// takes a version string and outputs the different components // takes a version string and outputs the different components
// //
// a component is delimited by '-' or '.' and consists of just digits or letters // a component is delimited by '-' or '.' and consists of just digits or letters
struct VersionComponentIterator<'a> { struct VersionComponentIterator<'a> {
v: &'a [u8], v: &'a [u8],
pos: usize, pos: usize,
} }
impl<'a> VersionComponentIterator<'a> { impl<'a> VersionComponentIterator<'a> {
pub fn new<I: Into<&'a str>>(v: I) -> Self { pub fn new<I: Into<&'a str>>(v: I) -> Self {
Self { Self {
v: v.into().as_bytes(), v: v.into().as_bytes(),
pos: 0, pos: 0,
}
} }
}
} }
impl Iterator for VersionComponentIterator<'_> { impl Iterator for VersionComponentIterator<'_> {
type Item = VersionComponent; type Item = VersionComponent;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
// skip all '-' and '.' in the beginning // skip all '-' and '.' in the beginning
while let Some(b'.' | b'-') = self.v.get(self.pos) { while let Some(b'.' | b'-') = self.v.get(self.pos) {
self.pos += 1; self.pos += 1;
}
// get the next character and decide if it is a digit or char
let c = self.v.get(self.pos)?;
let is_digit = c.is_ascii_digit();
// based on this collect characters after this into the component
let component_len = self.v[self.pos..]
.iter()
.copied()
.take_while(|&c| c.is_ascii_digit() == is_digit && c != b'.' && c != b'-')
.count();
let component =
String::from_utf8_lossy(&self.v[self.pos..(self.pos + component_len)]).into_owned();
// remember what chars we used
self.pos += component_len;
if component.is_empty() {
None
} else if is_digit {
component.parse::<u64>().ok().map(VersionComponent::Number)
} else {
Some(VersionComponent::Text(component))
}
} }
// get the next character and decide if it is a digit or char
let c = self.v.get(self.pos)?;
let is_digit = c.is_ascii_digit();
// based on this collect characters after this into the component
let component_len = self.v[self.pos..]
.iter()
.copied()
.take_while(|&c| c.is_ascii_digit() == is_digit && c != b'.' && c != b'-')
.count();
let component =
String::from_utf8_lossy(&self.v[self.pos..(self.pos + component_len)])
.into_owned();
// remember what chars we used
self.pos += component_len;
if component.is_empty() {
None
} else if is_digit {
component.parse::<u64>().ok().map(VersionComponent::Number)
} else {
Some(VersionComponent::Text(component))
}
}
} }
/// Compares two strings of package versions, and figures out the greater one. /// Compares two strings of package versions, and figures out the greater one.
@ -98,149 +108,154 @@ impl Iterator for VersionComponentIterator<'_> {
/// ///
/// * Ordering /// * Ordering
pub fn compare_versions(a: &str, b: &str) -> Ordering { pub fn compare_versions(a: &str, b: &str) -> Ordering {
let iter_a = VersionComponentIterator::new(a); let iter_a = VersionComponentIterator::new(a);
let iter_b = VersionComponentIterator::new(b); let iter_b = VersionComponentIterator::new(b);
iter_a.cmp(iter_b) iter_a.cmp(iter_b)
} }
/// Parses a nix store path to extract the packages name and version /// Parses a nix store path to extract the packages name and version
/// ///
/// This function first drops the inputs first 44 chars, since that is exactly the length of the /nix/store/... prefix. Then it matches that against our store path regex. /// This function first drops the inputs first 44 chars, since that is exactly
/// the length of the /nix/store/... prefix. Then it matches that against our
/// store path regex.
/// ///
/// # Returns /// # Returns
/// ///
/// * Result<(&'a str, &'a str)> - The Package's name and version, or an error if /// * Result<(&'a str, &'a str)> - The Package's name and version, or an error
/// one or both cannot be retrieved. /// if one or both cannot be retrieved.
pub fn get_version<'a>(pack: impl Into<&'a str>) -> Result<(&'a str, &'a str)> { pub fn get_version<'a>(pack: impl Into<&'a str>) -> Result<(&'a str, &'a str)> {
let path = pack.into(); let path = pack.into();
// We can strip the path since it _always_ follows the format // We can strip the path since it _always_ follows the format
// /nix/store/<...>-<program_name>-...... // /nix/store/<...>-<program_name>-......
// This part is exactly 44 chars long, so we just remove it. // This part is exactly 44 chars long, so we just remove it.
let stripped_path = &path[44..]; let stripped_path = &path[44..];
debug!("Stripped path: {stripped_path}"); debug!("Stripped path: {stripped_path}");
// Match the regex against the input // Match the regex against the input
if let Some(cap) = store_path_regex().captures(stripped_path) { if let Some(cap) = store_path_regex().captures(stripped_path) {
// Handle potential missing captures safely // Handle potential missing captures safely
let name = cap.get(1).map_or("", |m| m.as_str()); let name = cap.get(1).map_or("", |m| m.as_str());
let mut version = cap.get(2).map_or("<none>", |m| m.as_str()); let mut version = cap.get(2).map_or("<none>", |m| m.as_str());
if version.starts_with('-') { if version.starts_with('-') {
version = &version[1..]; version = &version[1..];
}
if name.is_empty() {
return Err(AppError::ParseError {
message: format!("Failed to extract name from path: {path}"),
context: "get_version".to_string(),
source: None,
});
}
return Ok((name, version));
} }
Err(AppError::ParseError { if name.is_empty() {
message: format!("Path does not match expected nix store format: {path}"), return Err(AppError::ParseError {
message: format!("Failed to extract name from path: {path}"),
context: "get_version".to_string(), context: "get_version".to_string(),
source: None, source: None,
}) });
}
return Ok((name, version));
}
Err(AppError::ParseError {
message: format!("Path does not match expected nix store format: {path}"),
context: "get_version".to_string(),
source: None,
})
} }
// Returns a reference to the compiled regex pattern. // Returns a reference to the compiled regex pattern.
// The regex is compiled only once. // The regex is compiled only once.
pub fn store_path_regex() -> &'static Regex { pub fn store_path_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new(); static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| { REGEX.get_or_init(|| {
Regex::new(r"(.+?)(-([0-9].*?))?$") Regex::new(r"(.+?)(-([0-9].*?))?$")
.expect("Failed to compile regex pattern for nix store paths") .expect("Failed to compile regex pattern for nix store paths")
}) })
} }
// TODO: move this somewhere else, this does not really // TODO: move this somewhere else, this does not really
// belong into this file // belong into this file
pub struct PackageDiff<'a> { pub struct PackageDiff<'a> {
pub pkg_to_versions_pre: HashMap<&'a str, HashSet<&'a str>>, pub pkg_to_versions_pre: HashMap<&'a str, HashSet<&'a str>>,
pub pkg_to_versions_post: HashMap<&'a str, HashSet<&'a str>>, pub pkg_to_versions_post: HashMap<&'a str, HashSet<&'a str>>,
pub pre_keys: HashSet<&'a str>, pub pre_keys: HashSet<&'a str>,
pub post_keys: HashSet<&'a str>, pub post_keys: HashSet<&'a str>,
pub added: HashSet<&'a str>, pub added: HashSet<&'a str>,
pub removed: HashSet<&'a str>, pub removed: HashSet<&'a str>,
pub changed: HashSet<&'a str>, pub changed: HashSet<&'a str>,
} }
impl<'a> PackageDiff<'a> { impl<'a> PackageDiff<'a> {
pub fn new<S: AsRef<str> + 'a>(pkgs_pre: &'a [S], pkgs_post: &'a [S]) -> Self { pub fn new<S: AsRef<str> + 'a>(
// Map from packages of the first closure to their version pkgs_pre: &'a [S],
let mut pre = HashMap::<&str, HashSet<&str>>::new(); pkgs_post: &'a [S],
let mut post = HashMap::<&str, HashSet<&str>>::new(); ) -> Self {
// Map from packages of the first closure to their version
let mut pre = HashMap::<&str, HashSet<&str>>::new();
let mut post = HashMap::<&str, HashSet<&str>>::new();
for p in pkgs_pre { for p in pkgs_pre {
match get_version(p.as_ref()) { match get_version(p.as_ref()) {
Ok((name, version)) => { Ok((name, version)) => {
pre.entry(name).or_default().insert(version); pre.entry(name).or_default().insert(version);
} },
Err(e) => { Err(e) => {
debug!("Error parsing package version: {e}"); debug!("Error parsing package version: {e}");
} },
} }
}
for p in pkgs_post {
match get_version(p.as_ref()) {
Ok((name, version)) => {
post.entry(name).or_default().insert(version);
}
Err(e) => {
debug!("Error parsing package version: {e}");
}
}
}
// Compare the package names of both versions
let pre_keys: HashSet<&str> = pre.keys().copied().collect();
let post_keys: HashSet<&str> = post.keys().copied().collect();
// Difference gives us added and removed packages
let added: HashSet<&str> = &post_keys - &pre_keys;
let removed: HashSet<&str> = &pre_keys - &post_keys;
// Get the intersection of the package names for version changes
let changed: HashSet<&str> = &pre_keys & &post_keys;
Self {
pkg_to_versions_pre: pre,
pkg_to_versions_post: post,
pre_keys,
post_keys,
added,
removed,
changed,
}
} }
for p in pkgs_post {
match get_version(p.as_ref()) {
Ok((name, version)) => {
post.entry(name).or_default().insert(version);
},
Err(e) => {
debug!("Error parsing package version: {e}");
},
}
}
// Compare the package names of both versions
let pre_keys: HashSet<&str> = pre.keys().copied().collect();
let post_keys: HashSet<&str> = post.keys().copied().collect();
// Difference gives us added and removed packages
let added: HashSet<&str> = &post_keys - &pre_keys;
let removed: HashSet<&str> = &pre_keys - &post_keys;
// Get the intersection of the package names for version changes
let changed: HashSet<&str> = &pre_keys & &post_keys;
Self {
pkg_to_versions_pre: pre,
pkg_to_versions_post: post,
pre_keys,
post_keys,
added,
removed,
changed,
}
}
} }
mod test { mod test {
#[test] #[test]
fn test_version_component_iter() { fn test_version_component_iter() {
use super::VersionComponent::{Number, Text}; use super::VersionComponent::{
use crate::util::VersionComponentIterator; Number,
let v = "132.1.2test234-1-man----.--.......---------..---"; Text,
};
use crate::util::VersionComponentIterator;
let v = "132.1.2test234-1-man----.--.......---------..---";
let comp: Vec<_> = VersionComponentIterator::new(v).collect(); let comp: Vec<_> = VersionComponentIterator::new(v).collect();
assert_eq!( assert_eq!(comp, [
comp, Number(132),
[ Number(1),
Number(132), Number(2),
Number(1), Text("test".into()),
Number(2), Number(234),
Text("test".into()), Number(1),
Number(234), Text("man".into())
Number(1), ]);
Text("man".into()) }
]
);
}
} }