Skip to content

Conversation

@kiliankoe
Copy link

@kiliankoe kiliankoe commented Aug 23, 2025

Hi!

I'm still fairly new to the nix ecosystem and nh as well. I've been using it for a short while now and have come to love its usability and diff outputs after building my config. I also manage homebrew (for GUI apps) through my nix-darwin installation, and have dearly missed seeing a proposed diff from that after building. After investigating a bit, I decided to hack together brewdiff with an API that tries to match dix so that the integration here is straight-forward.

The output can look something like this:

<<< /run/current-system
>>> /nix/store/bl8083swf2j0dgsalnnn49v7ib0nwlix-darwin-system-25.11.8df64f8

CHANGED
[... dix diff]

SIZE: 3.60 GiB -> 3.65 GiB
DIFF: 54.7 MiB

HOMEBREW: 1 added, 2 removed

ADDED
Casks
[A] figma
App Store
[A] Pieoneer (6739781207)

REMOVED
Formulae
[R] mas
Casks
[R] calibre

It lists diffs for casks (GUI apps) and formulae (command-line tools and libraries).
It also does Mac App Store diffs which can be configured through nix-darwin's homebrew config, but homebrew doesn't handle this by itself, passing it on to mas. This unfortunately doesn't uninstall removals automatically, so brewdiff only lists additions here to match that.

I hope this matches the rest of the implementation! Cheers!

Sanity Checking

  • I have updated the changelog as per my changes
  • I have tested, and self-reviewed my code
  • Style and consistency
    • I ran nix fmt to format my Nix code
    • I ran cargo fmt to format my Rust code
    • I have added appropriate documentation to new code
    • My changes are consistent with the rest of the codebase
  • Correctness
    • I ran cargo clippy and fixed any new linter warnings.
  • If new changes are particularly complex:
    • My code includes comments in particularly complex areas to explain the
      logic
    • I have documented the motive for those changes in the PR body or commit
      description.
  • Tested on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin

Add a 👍 reaction to pull requests you find important.

Summary by CodeRabbit

  • New Features

    • On macOS, nh darwin now shows Homebrew changes (formulae, casks, taps, Mac App Store apps) alongside Nix changes during diffs when Homebrew is managed by nix-darwin. Gracefully skips if Homebrew isn’t available or configured.
  • Documentation

    • Updated changelog to describe the new Homebrew diff visibility.
  • Chores

    • Added a macOS-only dependency to enable Homebrew change detection.

@coderabbitai
Copy link

coderabbitai bot commented Sep 8, 2025

Walkthrough

Adds macOS-only Homebrew diff support to the darwin rebuild diff flow. Introduces brewdiff dependency, a new util::print_homebrew_diff function (macOS implementation and non-macOS stub), imports and invokes it alongside the existing Nix diff, and documents the behavior in CHANGELOG.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md
Adds Unreleased entry noting nh darwin shows Homebrew package changes (formulae, casks, taps, Mac App Store apps) alongside Nix changes when nix-darwin manages Homebrew.
macOS dependency
Cargo.toml
Adds brewdiff = "0.2.0" under [target.'cfg(target_os="macos")'.dependencies]; other platforms unaffected.
Darwin rebuild flow
src/darwin.rs
Imports print_homebrew_diff and invokes it in the diff path after print_dix_diff, using the same profiles and preserving control flow.
Homebrew diff utility
src/util.rs
Adds pub fn print_homebrew_diff(...) -> Result<()>: macOS implementation computes and prints Homebrew diffs if available/configured; non-macOS stub returns Ok(()). Adds homebrew_available() (macOS). Errors are logged; function is non-fatal.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant D as darwin rebuild
  participant UTL as util
  participant N as print_dix_diff
  participant H as print_homebrew_diff
  participant B as brewdiff (macOS)

  U->>D: nh darwin --diff switch
  D->>N: print_dix_diff(old_profile, new_profile)
  N-->>D: nix pkg diff printed

  D->>H: print_homebrew_diff(old_profile, new_profile)
  alt macOS with Homebrew managed
    H->>H: Check brew in PATH and intent
    H->>B: Compute Homebrew diff
    B-->>H: Changes/no changes
    H-->>D: Print stats and diff if changes
  else non-macOS or not applicable
    H-->>D: No-op / log and return Ok(())
  end

  D-->>U: Diff output complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks (3 passed)

✅ Passed Checks (3 passed)
Check Name Status Explanation
Title Check ✅ Passed The title “darwin: show homebrew diff if available” succinctly captures the primary change—displaying a Homebrew diff in the Darwin build process—using concise, descriptive language aligned with the codebase’s naming style without extraneous detail.
Description Check ✅ Passed The description provides thorough context and rationale, links to the companion brewdiff tool, illustrates example output, details testing and formatting steps, and clearly explains why and how the feature was implemented.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  - Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.
  - Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
Cargo.toml (1)

30-30: Scope brewdiff to macOS target
brewdiff v0.2.0 exposes only macOS-specific APIs (extract_nix_darwin_intent, spawn_homebrew_diff, write_homebrew_stats, brewdiff::display::write_diff); move it under [target.'cfg(target_os="macos")'.dependencies] to avoid unnecessary builds:

 [dependencies]
 anstyle = "1.0.11"
-brewdiff = "0.2.0"
 chrono = "0.4.41"
@@
 [target.'cfg(target_os="macos")'.dependencies]
 system-configuration = "0.6.1"
+brewdiff = "0.2.0"
CHANGELOG.md (1)

19-25: Set user expectations about MAS limitation

Consider adding a short note that MAS removals aren't shown (only additions), to match current behavior.

src/util.rs (2)

341-406: Graceful, macOS-gated Homebrew diff looks solid

Nice separation, error downgrades to debug, and clean output integration.

Optional: align the “no changes” log level with print_dix_diff (info vs debug) for consistency.


408-420: Use the which crate instead of spawning which

Avoids an external process and simplifies the check.

Apply:

 #[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()
 }
src/darwin.rs (2)

20-20: Add brewdiff as a macOS-only dependency to avoid cross-platform build cost

Import looks fine. To keep non-darwin builds lean, ensure the brewdiff crate is only compiled on macOS via target-specific deps in Cargo.toml.

You can gate it like this in Cargo.toml:

+[target.'cfg(target_os = "macos")'.dependencies]
+brewdiff = "0.2.0"

145-146: Micro-nit: avoid PathBuf allocation; also consider deduping the repeated “Comparing…” debug line

No functional issues. Two small tweaks:

  • Use Path::new(CURRENT_PROFILE) to avoid an allocation.
  • There are two identical debug! lines printing the target profile (Lines 131–135 and 140–143). Consider removing one.

Apply for allocation nit:

-      let _ =
-        print_homebrew_diff(&PathBuf::from(CURRENT_PROFILE), &target_profile);
+      let _ =
+        print_homebrew_diff(std::path::Path::new(CURRENT_PROFILE), &target_profile);

(Optional) For the duplicate debug, you can drop the one inside the else-branch and keep the earlier one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 450cc45 and e0c9494.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • CHANGELOG.md (1 hunks)
  • Cargo.toml (1 hunks)
  • src/darwin.rs (2 hunks)
  • src/util.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/darwin.rs (1)
src/util.rs (2)
  • print_homebrew_diff (354-406)
  • print_homebrew_diff (424-429)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
🔇 Additional comments (2)
src/util.rs (2)

33-33: No-op formatting

Nothing to address.


422-429: Non-macOS stub is correct

API preserved and no-op behavior is appropriate.

@NotAShelf
Copy link
Member

AGI is here...

@kiliankoe
Copy link
Author

kiliankoe commented Sep 8, 2025

I assumed Coderabbit is an integration directly added to the project? Is that not the case? 🙈

Also, I don't know what is causing or what to do about that one CI failure. It's mentioning a formatting issue in Cargo.toml? But I don't see what it's referring to :/

@NotAShelf
Copy link
Member

I assumed Coderabbit is an integration directly added to the project? Is that not the case? 🙈

It was made available to the nix-community recently, I'm currently trying it out :) Not very keen on AI, hence the remark.

Also, I don't know what is causing or what to do about that one CI failure. It's mentioning a formatting issue in Cargo.toml? But I don't see what it's referring to :/

taplo fmt . should fix it. The CI logs tell you where it fails, but there are a lot of logs so hard to tell.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/darwin.rs (1)

136-147: Tweak debug message to reflect both diffs, not just dix.

Minor wording nit to avoid confusion when brew diff is also skipped.

Apply this diff:

-      debug!("Not running dix as the --diff flag is set to never.");
+      debug!("Not running diffs as the --diff flag is set to 'never'.");
src/util.rs (3)

353-406: Align error semantics with print_dix_diff (optional).

print_dix_diff returns Err on join panic; here we log and return Ok(()). Consider consistent behavior (propagate as Err) for easier upstream handling, even if callers ignore today.

Example:

-  let diff_data = match diff_handle.join() {
+  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(());
-    },
+    Ok(Err(e)) => return Err(eyre::eyre!(e)),
+    Err(_) => return Err(eyre::eyre!("Homebrew diff thread panicked")),
   };

355-357: Unused old_generation parameter: keep or document why.

You intentionally mirror print_dix_diff’s signature; consider noting that old_generation is unused (diff is intent vs current Homebrew state), or remove it in a future breaking change.


408-412: brew detection is fine; consider PATH caveat (optional).

Some users install Homebrew but don’t have brew in PATH in non-login shells. If this bites users, consider also checking common locations (/opt/homebrew/bin/brew, /usr/local/bin/brew).

 fn homebrew_available() -> bool {
-  which::which("brew").is_ok()
+  which::which("brew").is_ok()
+    || ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]
+      .iter()
+      .any(|p| std::path::Path::new(p).exists())
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 450cc45 and 3f5e9c0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • CHANGELOG.md (1 hunks)
  • Cargo.toml (1 hunks)
  • src/darwin.rs (2 hunks)
  • src/util.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/darwin.rs (1)
src/util.rs (4)
  • get_hostname (185-215)
  • print_dix_diff (309-339)
  • print_homebrew_diff (354-406)
  • print_homebrew_diff (416-421)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Darwin
🔇 Additional comments (4)
Cargo.toml (1)

61-63: macOS-scoped dependency looks good; please fix Taplo formatting if CI flags it.

The cfg-targeted dep is appropriate. If CI complains, run Taplo locally in the repo root to normalize formatting.

Run locally:

  • taplo fmt .
src/darwin.rs (1)

145-146: Good integration: Homebrew diff is invoked alongside dix and remains non-fatal.

src/util.rs (2)

34-67: LGTM on Nix variant detection.

Reasonable defaults and graceful handling when output is missing.


353-406: Robust, gated Homebrew diff with graceful fallbacks.

Nice: gated on macOS, checks brew presence, exits quietly when not configured, and prints stats + diff only when changes exist.

Comment on lines +21 to +24
- `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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Clarify Mac App Store behavior (removals aren’t shown/uninstalled).

brewdiff/mas won’t uninstall MAS apps; removals aren’t reported. The current wording implies added or removed for MAS as well.

Apply this diff:

-  This shows formulae, casks, taps, and Mac App Store apps that will be
-  added or removed during a configuration switch.
+  This shows formulae, casks, taps, and Mac App Store apps that will be
+  added during a configuration switch (Mac App Store removals are not
+  uninstalled by `mas` and are therefore not shown).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `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.
- `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 during a configuration switch (Mac App Store removals are not
uninstalled by `mas` and are therefore not shown).
🤖 Prompt for AI Agents
In CHANGELOG.md around lines 21 to 24, the current entry implies Mac App Store
(MAS) apps will be both added and removed when using `nh darwin` with Homebrew
management; however brewdiff/mas does not uninstall MAS apps and does not report
removals. Update the text to explicitly state that MAS apps will be listed when
they will be added but removals are not reported or performed (i.e., MAS apps
are only shown for additions, not removals/uninstalls), and adjust the sentence
to mention formulae, casks, taps, and MAS apps with a parenthetical or clause
clarifying MAS removal behavior.

@kiliankoe
Copy link
Author

I believe I've integrated most of the sensible AI feedback here, it had some valid points.

What's the best way forward here? Is this a feature that could make its way into nh? I'd love for it to exist and believe it has minimal impact on those not on darwin or not using homebrew through nix.

I'd love some feedback :)

@NotAShelf
Copy link
Member

I meant to give you some feedback earlier, but I'm a little unsure how to proceed as

  1. I don't use Darwin myself and have no clue if this is a logical implementation or if better alternatives exist
  2. I think the double diff output is rather superfluous and introduces an inconsistency between platform commands and their outputs

That said I'm currently looking to hear from @faukah as the developer of dix (the diffing crate we use) to see whether he'd be interested in making Homebrew diffing a native feature so that we could handle the diff at once with uniform formatting. Also in the talks with nix-darwin users that I know to see if they can propose something better, or if this PR is good to go.

@kiliankoe
Copy link
Author

That sounds great, thank you! I'm happy to see this go either way, but more input from people more familiar with nix-darwin, package diffing and so on feels very helpful.

This was just the most minimal approach that I felt comfortable with on my own, more or less as a proof of concept if the output makes sense. For me it appears to work fine, and I very much enjoy seeing that diff, but any better integration into existing tooling is very welcome.

@faukah
Copy link

faukah commented Sep 13, 2025

Hey, author of dix here. I've taken a quick look at brewdiff and have a few notes. I fundamentally wouldn't be against adding something like this to dix, however:

brewdiff calls the brew command repeatedly to get things like user-installed formulae and so on. This is bound to be quite slow compared to the rest of dix, and my main goals for the tool are good diffing and speed. If we can get the same results in some other, quicker way, I'd be happy. For example, we query Nix's SQlite database to get the closure of a derivation, maybe something similar is possible for brew.

@kiliankoe
Copy link
Author

Thanks for the suggestion! I'll be on vacation for a few weeks, but will definitely look into reading necessary information directly from an underlying store after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants