From e0c9494072136356f0701e10c3067d8a8424f289 Mon Sep 17 00:00:00 2001 From: Kilian Koeltzsch Date: Sat, 23 Aug 2025 01:38:33 +0200 Subject: [PATCH 1/3] darwin: show homebrew diff if available --- CHANGELOG.md | 7 ++++ Cargo.lock | 14 ++++++++ Cargo.toml | 1 + src/darwin.rs | 4 ++- src/util.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e08542aa..c7d318ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ functionality, under the "Removed" section. ## Unreleased +### Added + +- `nh darwin` now displays Homebrew package changes alongside Nix package + changes when using nix-darwin configurations with Homebrew management enabled. + This shows formulae, casks, taps, and Mac App Store apps that will be + added or removed during a configuration switch. + ### Changed - Nh checks are now more robust in the sense that unnecessary features will not diff --git a/Cargo.lock b/Cargo.lock index 86f8a042..f8136b99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,19 @@ version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +[[package]] +name = "brewdiff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877eb05cf54889e3ac31b427ffe8d95f6c109cd7e7c5abc0004896dd168937ae" +dependencies = [ + "owo-colors", + "regex", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -1222,6 +1235,7 @@ name = "nh" version = "4.2.0-beta5" dependencies = [ "anstyle", + "brewdiff", "chrono", "clap", "clap-verbosity-flag", diff --git a/Cargo.toml b/Cargo.toml index edcde569..d7ce4bff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ path = "src/lib.rs" [dependencies] anstyle = "1.0.11" +brewdiff = "0.2.0" chrono = "0.4.41" clap.workspace = true clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ], default-features = false } diff --git a/src/darwin.rs b/src/darwin.rs index dbf00352..6f55c274 100644 --- a/src/darwin.rs +++ b/src/darwin.rs @@ -17,7 +17,7 @@ use crate::{ }, nixos::toplevel_for, update::update, - util::{get_hostname, print_dix_diff}, + util::{get_hostname, print_dix_diff, print_homebrew_diff}, }; const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system"; @@ -142,6 +142,8 @@ impl DarwinRebuildArgs { target_profile.display() ); let _ = print_dix_diff(&PathBuf::from(CURRENT_PROFILE), &target_profile); + let _ = + print_homebrew_diff(&PathBuf::from(CURRENT_PROFILE), &target_profile); } if self.common.ask && !self.common.dry && !matches!(variant, Build) { diff --git a/src/util.rs b/src/util.rs index 1652a598..83df2ebb 100644 --- a/src/util.rs +++ b/src/util.rs @@ -30,6 +30,7 @@ impl fmt::Write for WriteFmt { self.0.write_all(string.as_bytes()).map_err(|_| fmt::Error) } } + /// Get the Nix variant (cached) pub fn get_nix_variant() -> &'static NixVariant { NIX_VARIANT.get_or_init(|| { @@ -336,3 +337,93 @@ pub fn print_dix_diff( } Ok(()) } + +/// Prints the difference between Homebrew packages in darwin generations. +/// +/// # Arguments +/// +/// * `old_generation` - A reference to the path of the old/current generation. +/// * `new_generation` - A reference to the path of the new generation. +/// +/// # Returns +/// +/// Returns `Ok(())` if the operation completed successfully, or an error +/// wrapped in `eyre::Result` if something went wrong. Silently returns Ok(()) +/// if Homebrew is not available or not configured. +#[cfg(target_os = "macos")] +pub fn print_homebrew_diff( + _old_generation: &Path, + new_generation: &Path, +) -> Result<()> { + if !homebrew_available() { + debug!("Homebrew not found, skipping Homebrew diff"); + return Ok(()); + } + + // Try to extract the nix-darwin Homebrew intent from the new profile + // If this fails, it likely means Homebrew isn't configured in the profile + let nix_intent = match brewdiff::extract_nix_darwin_intent(new_generation) { + Ok(intent) => intent, + Err(e) => { + debug!("Could not extract Homebrew intent from profile: {}", e); + return Ok(()); + }, + }; + + if !nix_intent.has_packages() { + debug!("No Homebrew packages configured in profile, skipping diff"); + return Ok(()); + } + + let mut out = WriteFmt(io::stdout()); + + let diff_handle = brewdiff::spawn_homebrew_diff(new_generation.to_path_buf()); + let diff_data = match diff_handle.join() { + Ok(Ok(data)) => data, + Ok(Err(e)) => { + debug!("Failed to compute Homebrew diff: {}", e); + return Ok(()); + }, + Err(_) => { + debug!("Homebrew diff thread panicked"); + return Ok(()); + }, + }; + + if diff_data.has_changes() { + // Separator from dix' output + println!(); + + // Print statistics first to make it clear the details below belong to + // Homebrew + brewdiff::write_homebrew_stats(&mut out, &diff_data)?; + brewdiff::display::write_diff(&mut out, &diff_data)?; + } else { + debug!("No Homebrew package changes detected"); + } + + Ok(()) +} + +/// Checks if Homebrew is available on the system +#[cfg(target_os = "macos")] +fn homebrew_available() -> bool { + use std::process::Command as StdCommand; + + StdCommand::new("which") + .arg("brew") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +/// Stub for non-macOS platforms +#[cfg(not(target_os = "macos"))] +pub fn print_homebrew_diff( + _old_generation: &Path, + _new_generation: &Path, +) -> Result<()> { + Ok(()) +} From 9b1594e336512a7bf4094ff26b3da389b8f5f9c5 Mon Sep 17 00:00:00 2001 From: Kilian Koeltzsch Date: Mon, 8 Sep 2025 19:04:26 +0200 Subject: [PATCH 2/3] Move brewdiff to macOS target, use which instead of spawning, and align log level --- Cargo.toml | 2 +- src/util.rs | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d7ce4bff..9eff99c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ path = "src/lib.rs" [dependencies] anstyle = "1.0.11" -brewdiff = "0.2.0" chrono = "0.4.41" clap.workspace = true clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ], default-features = false } @@ -60,6 +59,7 @@ tracing-subscriber = { features = [ "env-filter", "registry", "std" ], version = which = "8.0.0" [target.'cfg(target_os="macos")'.dependencies] +brewdiff = "0.2.0" system-configuration = "0.6.1" [dev-dependencies] diff --git a/src/util.rs b/src/util.rs index 83df2ebb..0b73e225 100644 --- a/src/util.rs +++ b/src/util.rs @@ -399,7 +399,7 @@ pub fn print_homebrew_diff( brewdiff::write_homebrew_stats(&mut out, &diff_data)?; brewdiff::display::write_diff(&mut out, &diff_data)?; } else { - debug!("No Homebrew package changes detected"); + info!("No Homebrew package changes."); } Ok(()) @@ -408,15 +408,7 @@ pub fn print_homebrew_diff( /// Checks if Homebrew is available on the system #[cfg(target_os = "macos")] fn homebrew_available() -> bool { - use std::process::Command as StdCommand; - - StdCommand::new("which") - .arg("brew") - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) + which::which("brew").is_ok() } /// Stub for non-macOS platforms From 3f5e9c0cf06f2dab0671e379608ae5fd4299a6a3 Mon Sep 17 00:00:00 2001 From: Kilian Koeltzsch Date: Mon, 8 Sep 2025 22:23:46 +0200 Subject: [PATCH 3/3] chore: taplo format cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9eff99c0..ba2b2748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ tracing-subscriber = { features = [ "env-filter", "registry", "std" ], version = which = "8.0.0" [target.'cfg(target_os="macos")'.dependencies] -brewdiff = "0.2.0" +brewdiff = "0.2.0" system-configuration = "0.6.1" [dev-dependencies]