From ca6f8609c37d14e1018f7f02bf17aca06c8cae1c Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 10 Feb 2026 00:06:24 -0800 Subject: [PATCH] modules/homebrew: add shell integration options Add `enableBashIntegration`, `enableFishIntegration`, and `enableZshIntegration` options that evaluate `brew shellenv` to set up Homebrew's environment and shell completions. This automates the boilerplate that every nix-darwin Homebrew user currently writes manually. All three shells use `interactiveShellInit`, consistent with direnv and home-manager conventions. Fish additionally sets up completions paths in the same hook. --- modules/homebrew.nix | 39 ++++++++++++++++++++++++++++ release.nix | 1 + tests/homebrew-shell-integration.nix | 29 +++++++++++++++++++++ tests/homebrew.nix | 3 +++ 4 files changed, 72 insertions(+) create mode 100644 tests/homebrew-shell-integration.nix diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 788a041..45e4d98 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -40,6 +40,11 @@ let # 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; @@ -587,6 +592,13 @@ in ''; }; + # 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 = { }; @@ -840,6 +852,33 @@ in 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.activationScripts.homebrew.text = mkIf cfg.enable '' # Homebrew Bundle echo >&2 "Homebrew bundle..." diff --git a/release.nix b/release.nix index b8a1d6b..8cdc62b 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-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-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 ed9a5fd..14d29f4 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -125,5 +125,8 @@ in echo "checking vscode entries in Brewfile" >&2 ${mkTest "golang.go" ''vscode "golang.go"''} + + echo "checking that shell integration is absent by default" >&2 + (! grep 'brew shellenv' ${config.out}/etc/zshrc) ''; }