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.
This commit is contained in:
Malo Bourgon 2026-02-10 00:06:24 -08:00
parent 8c29e146dd
commit ca6f8609c3
No known key found for this signature in database
4 changed files with 72 additions and 0 deletions

View file

@ -40,6 +40,11 @@ let
# Option and submodule helper functions ---------------------------------------------------------- # 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 // { mkNullOrBoolOption = args: mkOption (args // {
type = types.nullOr types.bool; type = types.nullOr types.bool;
default = null; 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 { onActivation = mkOption {
type = types.submodule onActivationOptions; type = types.submodule onActivationOptions;
default = { }; default = { };
@ -840,6 +852,33 @@ in
environment.variables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables; 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 '' system.activationScripts.homebrew.text = mkIf cfg.enable ''
# Homebrew Bundle # Homebrew Bundle
echo >&2 "Homebrew bundle..." echo >&2 "Homebrew bundle..."

View file

@ -83,6 +83,7 @@ in {
tests.environment-path = makeTest ./tests/environment-path.nix; tests.environment-path = makeTest ./tests/environment-path.nix;
tests.environment-terminfo = makeTest ./tests/environment-terminfo.nix; tests.environment-terminfo = makeTest ./tests/environment-terminfo.nix;
tests.homebrew = makeTest ./tests/homebrew.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-daemons = makeTest ./tests/launchd-daemons.nix;
tests.launchd-setenv = makeTest ./tests/launchd-setenv.nix; tests.launchd-setenv = makeTest ./tests/launchd-setenv.nix;
tests.networking-firewall = makeTest ./tests/networking-firewall.nix; tests.networking-firewall = makeTest ./tests/networking-firewall.nix;

View file

@ -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
'';
}

View file

@ -125,5 +125,8 @@ in
echo "checking vscode entries in Brewfile" >&2 echo "checking vscode entries in Brewfile" >&2
${mkTest "golang.go" ''vscode "golang.go"''} ${mkTest "golang.go" ''vscode "golang.go"''}
echo "checking that shell integration is absent by default" >&2
(! grep 'brew shellenv' ${config.out}/etc/zshrc)
''; '';
} }