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

wip: compiles

This commit is contained in:
RGBCube 2025-05-08 22:40:25 +03:00 committed by bloxx12
parent fc73fa9722
commit faa6634142
8 changed files with 389 additions and 381 deletions

33
Cargo.lock generated
View file

@ -73,7 +73,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.1.19",
"libc", "libc",
"winapi", "winapi",
] ]
@ -144,6 +144,16 @@ dependencies = [
"clap_derive", "clap_derive",
] ]
[[package]]
name = "clap-verbosity-flag"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84"
dependencies = [
"clap 4.5.37",
"log",
]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.37" version = "4.5.37"
@ -305,6 +315,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap 4.5.37", "clap 4.5.37",
"clap-verbosity-flag",
"criterion", "criterion",
"derive_more", "derive_more",
"diff", "diff",
@ -405,6 +416,23 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hermit-abi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.1",
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -1026,3 +1054,6 @@ name = "yansi"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
dependencies = [
"is-terminal",
]

View file

@ -1,11 +1,13 @@
[package] [package]
name = "dix" name = "dix"
description = "Diff Nix"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
clap = { version = "4.5.37", features = [ "derive" ] } clap = { version = "4.5.37", features = [ "derive" ] }
clap-verbosity-flag = "3.0.2"
derive_more = { version = "2.0.1", features = [ "full" ] } derive_more = { version = "2.0.1", features = [ "full" ] }
diff = "0.1.13" diff = "0.1.13"
env_logger = "0.11.3" env_logger = "0.11.3"
@ -15,7 +17,7 @@ regex = "1.11.1"
rusqlite = { version = "0.35.0", features = [ "bundled" ] } rusqlite = { version = "0.35.0", features = [ "bundled" ] }
rustc-hash = "2.1.1" rustc-hash = "2.1.1"
thiserror = "2.0.12" thiserror = "2.0.12"
yansi = "1.0.1" yansi = { version = "1.0.1", features = [ "detect-env", "detect-tty" ] }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"

101
src/diff.rs Normal file
View file

@ -0,0 +1,101 @@
use std::{
fmt,
io,
};
use rustc_hash::{
FxBuildHasher,
FxHashMap,
};
use yansi::Paint as _;
use crate::{
StorePath,
Version,
};
#[derive(Default)]
struct Diff<T> {
old: T,
new: T,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DiffStatus {
Added,
Removed,
Changed,
}
impl fmt::Display for DiffStatus {
fn fmt(&self, writer: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
writer,
"[{letter}]",
letter = match *self {
DiffStatus::Added => "A".green(),
DiffStatus::Removed => "R".red(),
DiffStatus::Changed => "C".yellow(),
},
)
}
}
pub fn diff<'a>(
writer: &mut dyn io::Write,
paths_old: impl Iterator<Item = &'a StorePath>,
paths_new: impl Iterator<Item = &'a StorePath>,
) -> io::Result<()> {
let mut paths =
FxHashMap::<&str, Diff<Vec<Option<&Version>>>>::with_hasher(FxBuildHasher);
for path in paths_old {
match path.parse_name_and_version() {
Ok((name, version)) => {
paths.entry(name).or_default().old.push(version);
},
Err(error) => {
log::info!("error parsing old path name and version: {error}");
},
}
}
for path in paths_new {
match path.parse_name_and_version() {
Ok((name, version)) => {
paths.entry(name).or_default().new.push(version);
},
Err(error) => {
log::info!("error parsing new path name and version: {error}");
},
}
}
let mut diffs = paths
.into_iter()
.filter_map(|(name, versions)| {
let status = match (versions.old.len(), versions.new.len()) {
(0, 0) => unreachable!(),
(0, _) => DiffStatus::Removed,
(_, 0) => DiffStatus::Added,
(..) if versions.old != versions.new => DiffStatus::Changed,
(..) => return None,
};
Some((name, versions, status))
})
.collect::<Vec<_>>();
diffs.sort_by(|&(a_name, _, a_status), &(b_name, _, b_status)| {
a_status.cmp(&b_status).then_with(|| a_name.cmp(b_name))
});
for (name, _versions, status) in diffs {
write!(writer, "{status} {name}")?;
writeln!(writer)?;
}
Ok(())
}

View file

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

View file

@ -12,8 +12,8 @@ use anyhow::{
}; };
use derive_more::Deref; use derive_more::Deref;
// pub mod diff; mod diff;
// pub mod print; pub use diff::diff;
pub mod store; pub mod store;
@ -24,7 +24,7 @@ pub use version::Version;
#[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DerivationId(i64); pub struct DerivationId(i64);
#[derive(Deref, Debug, Clone, PartialEq, Eq)] #[derive(Deref, Debug, Clone, PartialEq, Eq, Hash)]
pub struct StorePath(PathBuf); pub struct StorePath(PathBuf);
impl TryFrom<PathBuf> for StorePath { impl TryFrom<PathBuf> for StorePath {

View file

@ -1,225 +1,229 @@
use core::str;
use std::{ use std::{
collections::HashSet, fmt::Write as _,
io::{
self,
Write as _,
},
path::PathBuf,
process,
thread, thread,
}; };
use clap::Parser; use anyhow::{
use dixlib::{ Context as _,
print, Error,
Result,
};
use clap::Parser as _;
use dix::{
diff,
store, store,
util::PackageDiff,
}; };
use log::{ use yansi::Paint as _;
debug,
error,
};
use yansi::Paint;
#[derive(Parser, Debug)] #[derive(clap::Parser, Debug)]
#[command(name = "dix")] #[command(version, about)]
#[command(version = "1.0")] struct Cli {
#[command(about = "Diff Nix stuff", long_about = None)] old_path: PathBuf,
#[command(version, about, long_about = None)] new_path: PathBuf,
struct Args {
path: std::path::PathBuf,
path2: std::path::PathBuf,
/// Print the whole store paths #[command(flatten)]
#[arg(short, long)] verbose: clap_verbosity_flag::Verbosity,
paths: bool,
/// 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)] fn real_main() -> Result<()> {
struct Package<'a> { let Cli {
name: &'a str, old_path,
versions: HashSet<&'a str>, new_path,
/// Save if a package is a dependency of another package verbose,
is_dep: bool, } = Cli::parse();
}
impl<'a> Package<'a> { yansi::whenever(yansi::Condition::TTY_AND_COLOR);
fn new(name: &'a str, version: &'a str, is_dep: bool) -> Self {
let mut versions = HashSet::new();
versions.insert(version);
Self {
name,
versions,
is_dep,
}
}
fn add_version(&mut self, version: &'a str) { env_logger::Builder::new()
self.versions.insert(version); .filter_level(verbose.log_level_filter())
}
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
fn main() {
let args = Args::parse();
// 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(); .init();
// handles to the threads collecting closure size information // Handle to the thread collecting closure size information.
// We do this as early as possible because nix is slow. // We do this as early as possible because Nix is slow.
let closure_size_handles = if args.closure_size { let _closure_size_handle = {
debug!("Calculating closure sizes in background"); log::debug!("calculating closure sizes in background");
let path = args.path.clone();
let path2 = args.path2.clone(); let mut connection = store::connect()?;
Some((
thread::spawn(move || store::get_closure_size(&path)), let old_path = old_path.clone();
thread::spawn(move || store::get_closure_size(&path2)), let new_path = new_path.clone();
thread::spawn(move || {
Ok::<_, Error>((
connection.query_closure_size(&old_path)?,
connection.query_closure_size(&new_path)?,
)) ))
} else {
None
};
// Get package lists and handle potential errors
let package_list_pre = match store::query_packages(&args.path) {
Ok(packages) => {
debug!("Found {} packages in first closure", packages.len());
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::query_packages(&args.path2) {
Ok(packages) => {
debug!("Found {} packages in second closure", packages.len());
packages.into_iter().map(|(_, path)| path).collect()
},
Err(e) => {
error!(
"Error getting packages from path {}: {}",
args.path2.display(),
e
);
eprintln!(
"Error getting packages from path {}: {}",
args.path2.display(),
e
);
Vec::new()
},
};
let PackageDiff {
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);
debug!("Added packages: {}", added.len());
debug!("Removed packages: {}", removed.len());
debug!(
"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,
}
}) })
.count() };
let mut connection = store::connect()?;
let paths_old =
connection.query_depdendents(&old_path).with_context(|| {
format!(
"failed to query dependencies of path '{path}'",
path = old_path.display()
)
})?;
log::debug!(
"found {count} packages in old closure",
count = paths_old.len(),
); );
println!("Difference between the two generations:"); let paths_new =
println!(); connection.query_depdendents(&new_path).with_context(|| {
format!(
"failed to query dependencies of path '{path}'",
path = new_path.display()
)
})?;
let width_changes = changed.iter().filter(|&&p| { log::debug!(
match (pre.get(p), post.get(p)) { "found {count} packages in new closure",
(Some(version_pre), Some(version_post)) => version_pre != version_post, count = paths_new.len(),
_ => false,
}
});
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"
); );
},
drop(connection);
let mut out = io::stdout();
#[expect(clippy::pattern_type_mismatch)]
diff(
&mut out,
paths_old.iter().map(|(_, path)| path),
paths_new.iter().map(|(_, path)| path),
)?;
// let PackageDiff {
// pkg_to_versions_pre: pre,
// pkg_to_versions_post: post,
// pre_keys: _,
// post_keys: _,
// added,
// removed,
// changed,
// } = PackageDiff::new(&packages_old, &packages_after);
// log::debug!("Added packages: {}", added.len());
// log::debug!("Removed packages: {}", removed.len());
// log::debug!(
// "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,
// }
// })
// .count()
// );
// println!("Difference between the two generations:");
// println!();
// let width_changes = changed.iter().filter(|&&p| {
// match (pre.get(p), post.get(p)) {
// (Some(version_pre), Some(version_post)) => version_pre != version_post,
// _ => false,
// }
// });
// let col_width = added
// .iter()
// .chain(removed.iter())
// .chain(width_changes)
// .map(|p| p.len())
// .max()
// .unwrap_or_default();
// println!("<<< {}", cli.path.to_string_lossy());
// println!(">>> {}", cli.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;
// log::debug!("Pre closure size: {pre_size} MiB");
// log::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))) => {
// log::error!("Error getting closure size: {e}");
// eprintln!("Error getting closure size: {e}");
// },
// _ => {
// log::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" );
// },
// }
// }
Ok(())
} }
#[allow(clippy::allow_attributes, clippy::exit)]
fn main() {
let Err(error) = real_main() else {
return;
};
let mut err = io::stderr();
let mut message = String::new();
let mut chain = error.chain().rev().peekable();
while let Some(error) = chain.next() {
let _ = write!(
err,
"{header} ",
header = if chain.peek().is_none() {
"error:"
} else {
"cause:"
} }
.red()
.bold(),
);
String::clear(&mut message);
let _ = write!(message, "{error}");
let mut chars = message.char_indices();
let _ = if let Some((_, first)) = chars.next()
&& let Some((second_start, second)) = chars.next()
&& second.is_lowercase()
{
writeln!(
err,
"{first_lowercase}{rest}",
first_lowercase = first.to_lowercase(),
rest = &message[second_start..],
)
} else {
writeln!(err, "{message}")
};
}
process::exit(1);
} }

View file

@ -200,7 +200,7 @@ pub fn print_changes(
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("(-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,4 +1,7 @@
use std::result; use std::{
path::Path,
result,
};
use anyhow::{ use anyhow::{
Context as _, Context as _,
@ -48,10 +51,36 @@ pub fn connect() -> Result<Connection> {
} }
impl Connection { impl Connection {
/// Gathers all derivations that the given store path depends on. /// 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(&mut self, path: &Path) -> 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 = self
.prepare_cached(QUERY)?
.query_row([path], |row| row.get(0))?;
Ok(closure_size)
}
/// Gathers all derivations that the given profile path depends on.
pub fn query_depdendents( pub fn query_depdendents(
&mut self, &mut self,
path: &StorePath, path: &Path,
) -> Result<Vec<(DerivationId, StorePath)>> { ) -> Result<Vec<(DerivationId, StorePath)>> {
const QUERY: &str = " const QUERY: &str = "
WITH RECURSIVE WITH RECURSIVE
@ -82,32 +111,6 @@ impl Connection {
Ok(packages?) 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(&mut self, 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 = self
.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 /// Gathers the complete dependency graph of of the store path as an adjacency
/// list. /// list.
/// ///