From c68f5d13873e94f4b27130ad1dca74dfd19a01e4 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 10 Feb 2026 01:46:53 -0800 Subject: [PATCH] modules/homebrew: add `onActivation.cleanup` "check" mode Closes #1032 Add `"check"` to the `onActivation.cleanup` enum. When set, nix-darwin runs `brew bundle cleanup` during system checks to detect Homebrew packages that are installed but not present in the generated Brewfile. If extra packages are found, activation fails with a list of them and remediation steps. Unlike `"uninstall"` and `"zap"`, the `"check"` mode never removes packages -- it only reports. This runs during both `darwin-rebuild check` and `darwin-rebuild switch`, matching the behavior of all other system checks. --- modules/homebrew.nix | 52 ++++++++++++++++++++++++++------ release.nix | 1 + tests/homebrew-cleanup-check.nix | 19 ++++++++++++ tests/homebrew.nix | 3 ++ 4 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 tests/homebrew-cleanup-check.nix diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 45e4d98..04c94bb 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -76,29 +76,36 @@ let onActivationOptions = { config, ... }: { options = { cleanup = mkOption { - type = types.enum [ "none" "uninstall" "zap" ]; + type = types.enum [ "none" "check" "uninstall" "zap" ]; default = "none"; example = "uninstall"; description = '' - This option manages what happens to formulae installed by Homebrew, that aren't present in + This option manages what happens to packages installed by Homebrew that aren't present in the Brewfile generated by this module, during {command}`nix-darwin` system activation. - When set to `"none"` (the default), formulae not present in the generated + When set to `"none"` (the default), packages not present in the generated Brewfile are left installed. + When set to `"check"`, {command}`nix-darwin` verifies during system activation that no + Homebrew packages (taps, formulae, casks, etc.) are installed that aren't present in the + generated Brewfile. If extra packages are found, activation fails with a list of them. + Note that when this check fails during {command}`darwin-rebuild switch`, the entire + system activation is aborted and no other configuration changes will be applied until + the issue is resolved. + When set to `"uninstall"`, {command}`nix-darwin` invokes {command}`brew bundle [install]` with the {command}`--cleanup` flag. This - uninstalls all formulae not listed in generated Brewfile, i.e., - {command}`brew uninstall` is run for those formulae. + uninstalls all packages not listed in the generated Brewfile, i.e., + {command}`brew uninstall` is run for those packages. When set to `"zap"`, {command}`nix-darwin` invokes {command}`brew bundle [install]` with the {command}`--cleanup --zap` - flags. This uninstalls all formulae not listed in the generated Brewfile, and if the - formula is a cask, removes all files associated with that cask. In other words, - {command}`brew uninstall --zap` is run for all those formulae. + flags. This uninstalls all packages not listed in the generated Brewfile, and if the + package is a cask, removes all files associated with that cask. In other words, + {command}`brew uninstall --zap` is run for all those packages. - If you plan on exclusively using {command}`nix-darwin` to manage formulae + If you plan on exclusively using {command}`nix-darwin` to manage packages installed by Homebrew, you probably want to set this option to `"uninstall"` or `"zap"`. ''; @@ -879,6 +886,33 @@ in ''; }; + system.checks.text = mkIf (cfg.enable && cfg.onActivation.cleanup == "check") '' + if [ -f "${cfg.prefix}/bin/brew" ]; then + homebrewCleanupExitCode=0 + homebrewCleanupResult=$(PATH="${cfg.prefix}/bin:${lib.makeBinPath [ pkgs.mas ]}:$PATH" \ + sudo \ + --preserve-env=PATH \ + --user=${escapeShellArg cfg.user} \ + --set-home \ + env HOMEBREW_NO_AUTO_UPDATE=1 \ + brew bundle cleanup --file='${brewfileFile}' 2>&1) || homebrewCleanupExitCode=$? + if [ "$homebrewCleanupExitCode" -eq 1 ]; then + printf >&2 '\e[1;31merror: found Homebrew packages not listed in the Brewfile, aborting activation\e[0m\n' + printf >&2 '%s\n' "$homebrewCleanupResult" + printf >&2 '\n' + printf >&2 'To fix this, either:\n' + printf >&2 ' - Add the listed packages to your nix-darwin Homebrew configuration\n' + printf >&2 ' - Remove them by running: brew bundle cleanup --force\n' + printf >&2 ' - Set homebrew.onActivation.cleanup to "uninstall" or "zap"\n' + exit 2 + elif [ "$homebrewCleanupExitCode" -ne 0 ]; then + printf >&2 '\e[1;31merror: brew bundle cleanup failed, aborting activation\e[0m\n' + printf >&2 '%s\n' "$homebrewCleanupResult" + exit 2 + fi + fi + ''; + system.activationScripts.homebrew.text = mkIf cfg.enable '' # Homebrew Bundle echo >&2 "Homebrew bundle..." diff --git a/release.nix b/release.nix index 8cdc62b..883fc2a 100644 --- a/release.nix +++ b/release.nix @@ -83,6 +83,7 @@ in { tests.environment-path = makeTest ./tests/environment-path.nix; tests.environment-terminfo = makeTest ./tests/environment-terminfo.nix; tests.homebrew = makeTest ./tests/homebrew.nix; + tests.homebrew-cleanup-check = makeTest ./tests/homebrew-cleanup-check.nix; tests.homebrew-shell-integration = makeTest ./tests/homebrew-shell-integration.nix; tests.launchd-daemons = makeTest ./tests/launchd-daemons.nix; tests.launchd-setenv = makeTest ./tests/launchd-setenv.nix; diff --git a/tests/homebrew-cleanup-check.nix b/tests/homebrew-cleanup-check.nix new file mode 100644 index 0000000..2a63481 --- /dev/null +++ b/tests/homebrew-cleanup-check.nix @@ -0,0 +1,19 @@ +{ config, ... }: + +{ + homebrew.enable = true; + homebrew.user = "test-homebrew-user"; + homebrew.onActivation.cleanup = "check"; + + test = '' + echo "checking that cleanup check is present in system checks" >&2 + grep 'brew bundle cleanup --file=' ${config.out}/activate + + echo "checking that brew bundle command does not have --cleanup flag" >&2 + if echo "${config.homebrew.onActivation.brewBundleCmd}" | grep -F -- '--cleanup' > /dev/null; then + echo "Expected no --cleanup flag in brewBundleCmd" + echo "Actual: ${config.homebrew.onActivation.brewBundleCmd}" + exit 1 + fi + ''; +} diff --git a/tests/homebrew.nix b/tests/homebrew.nix index 14d29f4..1552010 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -128,5 +128,8 @@ in echo "checking that shell integration is absent by default" >&2 (! grep 'brew shellenv' ${config.out}/etc/zshrc) + + echo "checking that cleanup check is absent by default" >&2 + (! grep 'brew bundle cleanup --file=' ${config.out}/activate) ''; }