diff --git a/CHANGELOG b/CHANGELOG index 0a2ebf4..94e9287 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,32 @@ +2026-02-10 +- Major changes to `homebrew` module + + `homebrew.brewPrefix` was renamed to `homebrew.prefix`, and its semantics + changed: the old option pointed to the bin directory (e.g., + `/opt/homebrew/bin`), while the new option points to the Homebrew prefix + (e.g., `/opt/homebrew`), matching `brew --prefix`. + + `homebrew.whalebrews` was removed. Whalebrew support was removed from + Homebrew Bundle in Homebrew 4.7.0 (Nov 2025), so `whalebrew` entries in a + Brewfile now cause `brew bundle` to fail. + + `homebrew.global.lockfiles` and `homebrew.global.noLock` no longer have any + effect. Homebrew Bundle removed lockfile support in Homebrew 4.4.0 (Oct 2024). + + `homebrew.onActivation.cleanup` now supports a `"check"` mode, which checks + for unlisted packages and aborts activation if any are found, without + removing them. + + Shell integration options were added: `homebrew.enableBashIntegration`, + `homebrew.enableFishIntegration`, and `homebrew.enableZshIntegration`. + + New Brewfile entry types were added: `homebrew.goPackages`, + `homebrew.cargoPackages`, and `homebrew.vscode`. + + New options were added for brews: `postinstall`, `link = "overwrite"`, and + `restart_service = "always"`. The `postinstall` option was also added for + casks. + 2025-01-30 - Previously, some nix-darwin options applied to the user running `darwin-rebuild`. As part of a long‐term migration to make diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 0da306c..10047b0 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -28,9 +28,23 @@ let mkBrewfileLineOptionsListString = attrs: concatStringsSep ", " (mapAttrsToList (n: v: "${n}: ${v}") attrs); + # Renders a Brewfile option that can be either a bool or a Ruby symbol (e.g. `:overwrite`). + mkBrewfileLineBoolOrSymbolString = name: config: sCfg: + optionalString (hasAttr name sCfg) ( + ", ${name}: " + ( + if isBool config.${name} then sCfg.${name} + else ":${config.${name}}" + ) + ); + # Option and submodule helper functions ---------------------------------------------------------- + mkShellIntegrationOption = shell: mkEnableOption '' + Homebrew ${shell} shell integration, which sets up Homebrew's environment + and shell completions + ''; + mkNullOrBoolOption = args: mkOption (args // { type = types.nullOr types.bool; default = null; @@ -62,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"`. ''; @@ -97,7 +118,7 @@ let {command}`nix-darwin` system activation. The default is `false` so that repeated invocations of {command}`darwin-rebuild switch` are idempotent. - Note that Homebrew auto-updates when it's been more then 5 minutes since it last updated. + Note that Homebrew auto-updates when it's been more than 5 minutes since it last updated. Although auto-updating is disabled by default during system activation, note that Homebrew will auto-update when you manually invoke certain Homebrew commands. To modify this @@ -155,14 +176,6 @@ let Whether to enable Homebrew to automatically use the Brewfile that this module generates in the Nix store, when you manually invoke {command}`brew bundle`. - Enabling this option will change the default value of - [](#opt-homebrew.global.lockfiles) to `false` since, with - this option enabled, {command}`brew bundle [install]` will default to using the - Brewfile that this module generates in the Nix store, unless you explicitly point it at - another Brewfile using the `--file` flag. As a result, it will try to - write the lockfile in the Nix store, and complain that it can't (though the command will - run successfully regardless). - Implementation note: when enabled, this option sets the `HOMEBREW_BUNDLE_FILE` environment variable to the path of the Brewfile that this module generates in the Nix store, by adding it to @@ -178,7 +191,7 @@ let {command}`brew tap`, and {command}`brew bundle [install]`. Note that Homebrew auto-updates when you manually invoke commands like the ones mentioned - above if it's been more then 5 minutes since it last updated. + above if it's been more than 5 minutes since it last updated. You may want to consider disabling this option if you have [](#opt-homebrew.onActivation.upgrade) enabled, and @@ -191,31 +204,13 @@ let [](#opt-environment.variables). ''; }; - lockfiles = mkOption { - type = types.bool; - default = !config.brewfile; - defaultText = literalExpression "!config.homebrew.global.brewfile"; - description = '' - Whether to enable Homebrew to generate lockfiles when you manually invoke - {command}`brew bundle [install]`. - - This option will default to `false` if - [](#opt-homebrew.global.brewfile) is enabled since, with that option enabled, - {command}`brew bundle [install]` will default to using the Brewfile that this - module generates in the Nix store, unless you explicitly point it at another Brewfile - using the `--file` flag. As a result, it will try to write the - lockfile in the Nix store, and complain that it can't (though the command will run - successfully regardless). - - Implementation note: when disabled, this option sets the - `HOMEBREW_BUNDLE_NO_LOCK` environment variable, by adding it to - [](#opt-environment.variables). - ''; - }; - - # The `noLock` option was replaced by `lockfiles`. Due to `homebrew.global` being a submodule, - # we can't use `mkRemovedOptionModule`, so we leave this option definition here, and trigger - # and error message with an assertion below if it's set by the user. + # `noLock` was the original option; `lockfiles` replaced it (with inverted semantics). + # Both are now dead: Homebrew Bundle removed lockfile support in Homebrew 4.4.0 + # (Oct 2024), so the `HOMEBREW_BUNDLE_NO_LOCK` env var and `--no-lock` CLI flag are + # ignored. We keep both definitions with null defaults to detect explicit user + # configuration and emit a warning below. We can't use `mkRemovedOptionModule` because + # `homebrew.global` is a submodule. + lockfiles = mkOption { visible = false; default = null; }; noLock = mkOption { visible = false; default = null; }; homebrewEnvironmentVariables = mkInternalOption { type = types.attrs; }; @@ -225,7 +220,6 @@ let homebrewEnvironmentVariables = { HOMEBREW_BUNDLE_FILE = mkIf config.brewfile "${brewfileFile}"; HOMEBREW_NO_AUTO_UPDATE = mkIf (!config.autoUpdate) "1"; - HOMEBREW_BUNDLE_NO_LOCK = mkIf (!config.lockfiles) "1"; }; }; }; @@ -253,6 +247,10 @@ let description = '' Whether to auto-update the tap even if it is not hosted on GitHub. By default, only taps hosted on GitHub are auto-updated (for performance reasons). + + Note: Homebrew Bundle accepts this option in Brewfile syntax but may silently ignore it + during installation. See [the Homebrew Bundle source](https://github.com/Homebrew/brew/tree/master/Library/Homebrew/bundle) + for current behavior. ''; }; @@ -385,19 +383,34 @@ let }; require_sha = mkNullOrBoolOption { description = '' - Whether to require cask(s) to have a checksum. + Whether to require casks to have a checksum. Homebrew's default is `false`. ''; }; no_quarantine = mkNullOrBoolOption { - description = "Whether to disable quarantining of downloads."; + description = '' + Whether to disable quarantining of downloads. + + Note: this option is deprecated in Homebrew and may be removed in a + future release. See [Homebrew/brew#20755](https://github.com/Homebrew/brew/issues/20755). + + Homebrew's default is `false`. + ''; }; no_binaries = mkNullOrBoolOption { - description = "Whether to disable linking of helper executables."; + description = '' + Whether to disable linking of helper executables. + + Homebrew's default is `false`. + ''; }; ignore_dependencies = mkNullOrBoolOption { - description = "Ignore casks dependencies in case you manage them extrenally"; + description = '' + Whether to ignore cask dependencies, e.g., when you manage them externally. + + Homebrew's default is `false`. + ''; }; brewfileLine = mkInternalOption { type = types.nullOr types.str; }; @@ -424,7 +437,7 @@ let type = with types; nullOr (listOf str); default = null; description = '' - Arguments flags to pass to {command}`brew install`. Values should not include the + Argument flags to pass to {command}`brew install`. Values should not include the leading `"--"`. ''; }; @@ -437,12 +450,14 @@ let ''; }; restart_service = mkOption { - type = with types; nullOr (either bool (enum [ "changed" ])); + type = with types; nullOr (either bool (enum [ "changed" "always" ])); default = null; description = '' Whether to run {command}`brew services restart` for the formula and register it to launch at login (or boot). If set to `"changed"`, the service will only - be restarted on version changes. + be restarted when the formula is newly installed or upgraded. If set to + `"always"`, the service will be restarted on every {command}`brew bundle` + run, even if nothing changed. Homebrew's default is `false`. ''; @@ -450,19 +465,34 @@ let start_service = mkNullOrBoolOption { description = '' Whether to run {command}`brew services start` for the formula and register it to - launch at login (or boot). + launch at login (or boot). Unlike {option}`restart_service`, this only starts + the service if it is not currently running, without restarting an already-running + service. Homebrew's default is `false`. ''; }; - link = mkNullOrBoolOption { + link = mkOption { + type = with types; nullOr (either bool (enum [ "overwrite" ])); + default = null; description = '' - Whether to link the formula to the Homebrew prefix. When this option is - `null`, Homebrew will use it's default behavior which is to link the - formula if it's currently unlinked and not keg-only, and to unlink the formula if it's - currently linked and keg-only. + Whether to link the formula to the Homebrew prefix. When set to `"overwrite"`, + existing symlinks will be overwritten ({command}`brew link --overwrite`). When this + option is `null`, Homebrew will use its default behavior, which is to link the formula + if it's currently unlinked and not keg-only, and to unlink the formula if it's currently + linked and keg-only. ''; }; + postinstall = mkNullOrStrOption { + description = '' + A shell command to run after the formula is installed or upgraded. The command is passed + to the system shell and only executes when the formula actually changed (was freshly + installed or upgraded), not on every {command}`brew bundle` run. + ''; + }; + # `version_file` is intentionally not exposed: it writes the installed version to a file + # path relative to the `brew bundle` working directory, which is not meaningful during + # nix-darwin system activation. brewfileLine = mkInternalOption { type = types.nullOr types.str; }; }; @@ -470,20 +500,14 @@ let config = let sCfg = mkProcessedSubmodConfig config; - sCfgSubset = removeAttrs sCfg [ "name" "restart_service" ]; + sCfgSubset = removeAttrs sCfg [ "name" "restart_service" "link" ]; in { brewfileLine = "brew ${sCfg.name}" + optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}" - # We need to handle the `restart_service` option seperately since it can be either a bool - # or `:changed` in the Brewfile. - + optionalString (sCfg ? restart_service) ( - ", restart_service: " + ( - if isBool config.restart_service then sCfg.restart_service - else ":${config.restart_service}" - ) - ); + + mkBrewfileLineBoolOrSymbolString "link" config sCfg + + mkBrewfileLineBoolOrSymbolString "restart_service" config sCfg; }; }; @@ -510,6 +534,13 @@ let itself. ''; }; + postinstall = mkNullOrStrOption { + description = '' + A shell command to run after the cask is installed or upgraded. The command is passed to + the system shell and only executes when the cask was actually installed or upgraded, not + on every {command}`brew bundle` run. + ''; + }; brewfileLine = mkInternalOption { type = types.nullOr types.str; }; }; @@ -533,21 +564,26 @@ in imports = [ (mkRenamedOptionModule [ "homebrew" "autoUpdate" ] [ "homebrew" "onActivation" "autoUpdate" ]) (mkRenamedOptionModule [ "homebrew" "cleanup" ] [ "homebrew" "onActivation" "cleanup" ]) + (mkRemovedOptionModule [ "homebrew" "brewPrefix" ] "`homebrew.brewPrefix` has been renamed to `homebrew.prefix` and its semantics changed: the old option pointed to the bin directory (e.g., `/opt/homebrew/bin`), while the new option points to the Homebrew prefix (e.g., `/opt/homebrew`), matching `brew --prefix`. Please replace `homebrew.brewPrefix` with `homebrew.prefix`, removing the trailing `/bin` if present.") + (mkRemovedOptionModule [ "homebrew" "whalebrews" ] "Whalebrew support was removed from Homebrew Bundle in Homebrew 4.7.0 (Nov 2025). `whalebrew` entries in a Brewfile now cause `brew bundle` to fail. Please manage Whalebrew images directly using the `whalebrew` CLI.") ]; options.homebrew = { enable = mkEnableOption '' {command}`nix-darwin` to manage installing/updating/upgrading Homebrew taps, formulae, - and casks, as well as Mac App Store apps and Docker containers, using Homebrew Bundle. + casks, Mac App Store apps, Visual Studio Code extensions, Go packages, and Cargo + crates using Homebrew Bundle. Note that enabling this option does not install Homebrew, see the Homebrew [website](https://brew.sh) for installation instructions. Use the [](#opt-homebrew.brews), [](#opt-homebrew.casks), - [](#opt-homebrew.masApps), [](#opt-homebrew.whalebrews), [](#opt-homebrew.vscode) options - to list the Homebrew formulae, casks, Mac App Store apps, Docker containers and Visual Studio - Code Extensions you'd like to install. Use the [](#opt-homebrew.taps) option, to make additional - formula repositories available to Homebrew. This module uses those options (along with the + [](#opt-homebrew.masApps), [](#opt-homebrew.vscode), + [](#opt-homebrew.goPackages), and [](#opt-homebrew.cargoPackages) options to list + the Homebrew formulae, casks, Mac App Store apps, Visual Studio Code extensions, + Go packages, and Cargo crates you'd like to install. Use the + [](#opt-homebrew.taps) option, to make additional formula repositories available to + Homebrew. This module uses those options (along with the [](#opt-homebrew.caskArgs) options) to generate a Brewfile that {command}`nix-darwin` passes to the {command}`brew bundle` command during system activation. @@ -572,20 +608,27 @@ in ''; }; - brewPrefix = mkOption { + prefix = mkOption { type = types.str; - default = if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew/bin" else "/usr/local/bin"; + default = if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew" else "/usr/local"; defaultText = literalExpression '' - if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew/bin" - else "/usr/local/bin" + if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew" + else "/usr/local" ''; description = '' - The path prefix where the {command}`brew` executable is located. This will be set to - the correct value based on your system's platform, and should only need to be changed if you - manually installed Homebrew in a non-standard location. + The Homebrew prefix directory, i.e., the value that {command}`brew --prefix` returns. + The default is automatically set based on your system's platform, and should only need + to be changed if you manually installed Homebrew in a non-standard location. ''; }; + # These default to `false` (unlike direnv, which defaults to `true`) because existing users + # likely already have `brew shellenv` in their dotfiles, and enabling by default would cause + # duplicate evaluation. + enableBashIntegration = mkShellIntegrationOption "Bash"; + enableFishIntegration = mkShellIntegrationOption "Fish"; + enableZshIntegration = mkShellIntegrationOption "Zsh"; + onActivation = mkOption { type = types.submodule onActivationOptions; default = { }; @@ -648,6 +691,8 @@ in description = '' Whether to always upgrade casks listed in [](#opt-homebrew.casks) regardless of whether it's unversioned or it updates itself. + + Homebrew's default is `false`. ''; }; @@ -674,6 +719,12 @@ in link = true; conflicts_with = [ "mysql" ]; } + + # `brew install`, run a post-install command on version changes + { + name = "postgresql@16"; + postinstall = "\''${HOMEBREW_PREFIX}/opt/postgresql@16/bin/postgres -D \''${HOMEBREW_PREFIX}/var/postgresql@16"; + } ] ''; description = '' @@ -705,6 +756,12 @@ in name = "opera"; greedy = true; } + + # `brew install --cask`, run a post-install command on install or upgrade + { + name = "google-cloud-sdk"; + postinstall = "\''${HOMEBREW_PREFIX}/bin/gcloud components update"; + } ] ''; description = '' @@ -739,21 +796,6 @@ in ''; }; - whalebrews = mkOption { - type = with types; listOf str; - default = [ ]; - example = [ "whalebrew/wget" ]; - description = '' - List of Docker images to install using {command}`whalebrew`. - - When this option is used, `"whalebrew"` is automatically added to - [](#opt-homebrew.brews). - - For more information on {command}`whalebrew` see: - [github.com/whalebrew/whalebrew](https://github.com/whalebrew/whalebrew). - ''; - }; - vscode = mkOption { type = with types; listOf str; default = [ ]; @@ -770,6 +812,31 @@ in ''; }; + goPackages = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "github.com/charmbracelet/crush" ]; + description = '' + List of Go packages to install using {command}`go install`. + + Homebrew will automatically install the {command}`go` formula if it is not already + installed. + ''; + }; + + cargoPackages = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "ripgrep" ]; + description = '' + List of Rust packages to install using {command}`cargo install`. + + Homebrew will automatically install the {command}`rust` formula if it is not already + installed. + ''; + }; + + extraConfig = mkOption { type = types.lines; default = ""; @@ -782,7 +849,7 @@ in brewfile = mkInternalOption { type = types.str; - description = "String reprensentation of the generated Brewfile useful for debugging."; + description = "String representation of the generated Brewfile useful for debugging."; }; }; @@ -791,22 +858,16 @@ in config = { - assertions = [ - # See comment above `homebrew.global.noLock` option declaration for why this is required. - { assertion = cfg.global.noLock == null; message = "The option `homebrew.global.noLock' was removed, use `homebrew.global.lockfiles' in it's place."; } - ]; - warnings = [ (mkIf (options.homebrew.autoUpdate.isDefined || options.homebrew.cleanup.isDefined) "The `homebrew' module no longer upgrades outdated formulae and apps by default during `nix-darwin' system activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.") + (mkIf (cfg.global.noLock != null || cfg.global.lockfiles != null) "The options `homebrew.global.noLock' and `homebrew.global.lockfiles' have been deprecated. Homebrew Bundle removed lockfile support in Homebrew 4.4.0 (Oct 2024), so these options no longer have any effect. Please remove them from your configuration.") + (mkIf (hasSuffix "/bin" cfg.prefix) "`homebrew.prefix` should be the Homebrew prefix directory (e.g., `/opt/homebrew`), not the bin directory. The value should match what `brew --prefix` returns. Did you mean to remove the trailing `/bin`?") ]; system.requiresPrimaryUser = mkIf (cfg.enable && options.homebrew.user.highestPrio == (mkOptionDefault {}).priority) [ "homebrew.enable" ]; - homebrew.brews = - optional (cfg.whalebrews != [ ]) "whalebrew"; - homebrew.brewfile = "# Created by `nix-darwin`'s `homebrew` module\n\n" + mkBrewfileSectionString "Taps" cfg.taps @@ -816,17 +877,72 @@ in + mkBrewfileSectionString "Casks" cfg.casks + mkBrewfileSectionString "Mac App Store apps" (mapAttrsToList (n: id: ''mas "${n}", id: ${toString id}'') cfg.masApps) - + mkBrewfileSectionString "Docker containers" (map (v: ''whalebrew "${v}"'') cfg.whalebrews) + mkBrewfileSectionString "Visual Studio Code extensions" (map (v: ''vscode "${v}"'') cfg.vscode) + + mkBrewfileSectionString "Go packages" (map (v: ''go "${v}"'') cfg.goPackages) + + mkBrewfileSectionString "Cargo packages" (map (v: ''cargo "${v}"'') cfg.cargoPackages) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); environment.variables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables; + programs = mkIf cfg.enable { + bash.interactiveShellInit = mkIf cfg.enableBashIntegration '' + eval "$(${cfg.prefix}/bin/brew shellenv bash)" + if [[ -r "${cfg.prefix}/etc/profile.d/bash_completion.sh" ]]; then + source "${cfg.prefix}/etc/profile.d/bash_completion.sh" + else + for COMPLETION in "${cfg.prefix}/etc/bash_completion.d/"*; do + [[ -r "$COMPLETION" ]] && source "$COMPLETION" + done + fi + ''; + + zsh.interactiveShellInit = mkIf cfg.enableZshIntegration '' + eval "$(${cfg.prefix}/bin/brew shellenv zsh)" + ''; + + fish.interactiveShellInit = mkIf cfg.enableFishIntegration '' + eval (${cfg.prefix}/bin/brew shellenv fish) + if test -d "${cfg.prefix}/share/fish/completions" + set -p fish_complete_path "${cfg.prefix}/share/fish/completions" + end + if test -d "${cfg.prefix}/share/fish/vendor_completions.d" + set -p fish_complete_path "${cfg.prefix}/share/fish/vendor_completions.d" + end + ''; + }; + + 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..." - if [ -f "${cfg.brewPrefix}/brew" ]; then - PATH="${cfg.brewPrefix}:${lib.makeBinPath [ pkgs.mas ]}:$PATH" \ + if [ -f "${cfg.prefix}/bin/brew" ]; then + PATH="${cfg.prefix}/bin:${lib.makeBinPath [ pkgs.mas ]}:$PATH" \ sudo \ --preserve-env=PATH \ --user=${escapeShellArg cfg.user} \ @@ -838,4 +954,8 @@ in fi ''; }; + + meta.maintainers = [ + lib.maintainers.malo or "malo" + ]; } diff --git a/modules/system/checks.nix b/modules/system/checks.nix index b4bf578..03d35c5 100644 --- a/modules/system/checks.nix +++ b/modules/system/checks.nix @@ -252,7 +252,7 @@ let ''; homebrewInstalled = '' - if [[ ! -f ${escapeShellArg config.homebrew.brewPrefix}/brew && -z "''${INSTALLING_HOMEBREW:-}" ]]; then + if [[ ! -f ${escapeShellArg config.homebrew.prefix}/bin/brew && -z "''${INSTALLING_HOMEBREW:-}" ]]; then echo "error: Using the homebrew module requires homebrew installed, aborting activation" >&2 echo "Homebrew doesn't seem to be installed. Please install homebrew separately." >&2 echo "You can install homebrew using the following command:" >&2 diff --git a/release.nix b/release.nix index b8a1d6b..883fc2a 100644 --- a/release.nix +++ b/release.nix @@ -83,6 +83,8 @@ 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; tests.networking-firewall = makeTest ./tests/networking-firewall.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-shell-integration.nix b/tests/homebrew-shell-integration.nix new file mode 100644 index 0000000..7d44ff3 --- /dev/null +++ b/tests/homebrew-shell-integration.nix @@ -0,0 +1,29 @@ +{ config, ... }: + +{ + homebrew.enable = true; + programs.bash.enable = true; + programs.zsh.enable = true; + programs.fish.enable = true; + + homebrew.user = "test-homebrew-user"; + + homebrew.enableBashIntegration = true; + homebrew.enableFishIntegration = true; + homebrew.enableZshIntegration = true; + + test = '' + echo >&2 "checking bash shell integration in /etc/bashrc" + grep 'brew shellenv bash' ${config.out}/etc/bashrc + echo >&2 "checking bash completions in /etc/bashrc" + grep 'bash_completion' ${config.out}/etc/bashrc + + echo >&2 "checking zsh shell integration in /etc/zshrc" + grep 'brew shellenv zsh' ${config.out}/etc/zshrc + + echo >&2 "checking fish shell integration in /etc/fish/config.fish" + grep 'brew shellenv fish' ${config.out}/etc/fish/config.fish + echo >&2 "checking fish completions in /etc/fish/config.fish" + grep 'fish_complete_path' ${config.out}/etc/fish/config.fish + ''; +} diff --git a/tests/homebrew.nix b/tests/homebrew.nix index 64e52c4..e026197 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -17,7 +17,7 @@ in homebrew.user = "test-homebrew-user"; - # Examples taken from https://github.com/Homebrew/homebrew-bundle + # Examples adapted from https://docs.brew.sh/Brew-Bundle-and-Brewfile homebrew.taps = [ "homebrew/cask" { @@ -41,7 +41,8 @@ in { name = "denji/nginx/nginx-full"; args = [ "with-rmtp" ]; - restart_service = "changed"; + link = "overwrite"; + restart_service = "always"; } { name = "mysql@5.6"; @@ -49,6 +50,10 @@ in link = true; conflicts_with = [ "mysql" ]; } + { + name = "postgresql@16"; + postinstall = "\${HOMEBREW_PREFIX}/opt/postgresql@16/bin/postgres -D \${HOMEBREW_PREFIX}/var/postgresql@16"; + } ]; homebrew.casks = [ @@ -61,6 +66,10 @@ in name = "opera"; greedy = true; } + { + name = "google-cloud-sdk"; + postinstall = "\${HOMEBREW_PREFIX}/bin/gcloud components update"; + } ]; homebrew.masApps = { @@ -68,14 +77,19 @@ in Xcode = 497799835; }; - homebrew.whalebrews = [ - "whalebrew/wget" - ]; - homebrew.vscode = [ "golang.go" ]; + homebrew.goPackages = [ + "github.com/charmbracelet/crush" + ]; + + homebrew.cargoPackages = [ + "ripgrep" + ]; + + test = '' bf=${lib.escapeShellArg config.homebrew.brewfile} @@ -89,22 +103,33 @@ in echo "checking brew entries in Brewfile" >&2 ${mkTest "imagemagick" ''brew "imagemagick"''} - ${mkTest "denji/nginx/nginx-full" ''brew "denji/nginx/nginx-full", args: ["with-rmtp"], restart_service: :changed''} + ${mkTest "denji/nginx/nginx-full" ''brew "denji/nginx/nginx-full", args: ["with-rmtp"], link: :overwrite, restart_service: :always''} ${mkTest "mysql@5.6" ''brew "mysql@5.6", conflicts_with: ["mysql"], link: true, restart_service: true''} + ${mkTest "postgresql@16" ''brew "postgresql@16", postinstall: "''${HOMEBREW_PREFIX}/opt/postgresql@16/bin/postgres -D ''${HOMEBREW_PREFIX}/var/postgresql@16"''} echo "checking cask entries in Brewfile" >&2 ${mkTest "google-chrome" ''cask "google-chrome"''} ${mkTest "firefox" ''cask "firefox", args: { appdir: "~/my-apps/Applications" }''} ${mkTest "opera" ''cask "opera", greedy: true''} + ${mkTest "google-cloud-sdk" ''cask "google-cloud-sdk", postinstall: "''${HOMEBREW_PREFIX}/bin/gcloud components update"''} echo "checking mas entries in Brewfile" >&2 ${mkTest "1Password for Safari" ''mas "1Password for Safari", id: 1569813296''} ${mkTest "Xcode" ''mas "Xcode", id: 497799835''} - echo "checking whalebrew entries in Brewfile" >&2 - ${mkTest "whalebrew/wget" ''whalebrew "whalebrew/wget"''} - echo "checking vscode entries in Brewfile" >&2 ${mkTest "golang.go" ''vscode "golang.go"''} + + echo "checking go entries in Brewfile" >&2 + ${mkTest "github.com/charmbracelet/crush" ''go "github.com/charmbracelet/crush"''} + + echo "checking cargo entries in Brewfile" >&2 + ${mkTest "ripgrep" ''cargo "ripgrep"''} + + 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) ''; }