From 53dd29f3818737897b1571ccab529d20ce296131 Mon Sep 17 00:00:00 2001 From: Josh Gibbs Date: Tue, 3 Feb 2026 21:22:21 -0800 Subject: [PATCH 01/16] remove duplicates from brewfile --- modules/homebrew.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 1bf899b..a7846fd 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -12,7 +12,7 @@ let mkBrewfileSectionString = heading: entries: optionalString (entries != [ ]) '' # ${heading} - ${concatMapStringsSep "\n" (v: v.brewfileLine or v) entries} + ${concatStringsSep "\n" (unique (map (v: v.brewfileLine or v) entries))} ''; From fdbfb1dc1b91f12ea06c4f0698ee0c899f90b6e6 Mon Sep 17 00:00:00 2001 From: "Frank Chiarulli Jr." Date: Sat, 14 Dec 2024 12:47:19 -0500 Subject: [PATCH 02/16] add support for installing vscode extensions via brew --- modules/homebrew.nix | 25 +++++++++++++++++++++---- tests/homebrew.nix | 7 +++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 7aee9e1..6b78ef0 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -539,10 +539,10 @@ in [website](https://brew.sh) for installation instructions. Use the [](#opt-homebrew.brews), [](#opt-homebrew.casks), - [](#opt-homebrew.masApps), and [](#opt-homebrew.whalebrews) options - to list the Homebrew formulae, casks, Mac App Store apps, and Docker containers 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.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.caskArgs) options) to generate a Brewfile that {command}`nix-darwin` passes to the {command}`brew bundle` command during system activation. @@ -734,6 +734,22 @@ in ''; }; + vscode = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "golang.go" ]; + description = '' + List of Visual Studio Code extensions to install using Homebrew Bundle. + + A compatible editor (Visual Studio Code, VSCodium, Cursor, or VS Code Insiders) + must be available. If none is found, Homebrew will attempt to install + `visual-studio-code` automatically. + + For more information on {command}`code` see: + [VSCode Extension Marketplace](https://code.visualstudio.com/docs/editor/extension-marketplace). + ''; + }; + extraConfig = mkOption { type = types.lines; default = ""; @@ -778,6 +794,7 @@ in + 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) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); environment.variables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables; diff --git a/tests/homebrew.nix b/tests/homebrew.nix index d7fdeab..dd8ce02 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -70,6 +70,10 @@ in "whalebrew/wget" ]; + homebrew.vscode = [ + "golang.go" + ]; + test = '' bf=${lib.escapeShellArg config.homebrew.brewfile} @@ -97,5 +101,8 @@ in 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"''} ''; } From 36815b4852dc9fd5defb500377998c0b6ed3d836 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 19:05:50 -0800 Subject: [PATCH 03/16] modules/homebrew: add restart_service "always" support Homebrew supports restart_service: :always which restarts the service on every brew bundle run, even if the formula wasn't changed. --- modules/homebrew.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 0da306c..eba43c6 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -437,12 +437,13 @@ 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 on version changes. If set to `"always"`, the service will + be restarted on every {command}`brew bundle` run, even if nothing changed. Homebrew's default is `false`. ''; From a3fd89f1bbb03bb580fd47f1ee7daa9018bba7cb Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 19:36:07 -0800 Subject: [PATCH 04/16] modules/homebrew: add `link: :overwrite` support Homebrew supports `link: :overwrite` which runs `brew link --overwrite`, force-overwriting existing symlinks. Extract the existing `restart_service` special-case logic into a reusable helper (`mkBrewfileLineBoolOrSymbolString`) for options that can be either a bool or a Ruby symbol in the Brewfile. --- modules/homebrew.nix | 34 ++++++++++++++++++++-------------- tests/homebrew.nix | 7 ++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index eba43c6..61fb5c8 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -28,6 +28,15 @@ 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 ---------------------------------------------------------- @@ -456,12 +465,15 @@ let 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. ''; }; @@ -471,20 +483,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; }; }; diff --git a/tests/homebrew.nix b/tests/homebrew.nix index 64e52c4..7815158 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"; @@ -89,7 +90,7 @@ 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''} echo "checking cask entries in Brewfile" >&2 From c65c24c87c23a59e5d401e97ddeb95c67c7cd869 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 20:21:29 -0800 Subject: [PATCH 05/16] modules/homebrew: add `postinstall` option for brews and casks Both `brew bundle` formula and cask installers support a `postinstall` option -- a shell command to run after the package is installed or upgraded. The command only executes when the package actually changed, not on every `brew bundle` run. Examples from the Homebrew docs added to the `homebrew.brews` and `homebrew.casks` option examples and tests. --- modules/homebrew.nix | 29 +++++++++++++++++++++++++++++ tests/homebrew.nix | 10 ++++++++++ 2 files changed, 39 insertions(+) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 61fb5c8..aadec07 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -476,6 +476,16 @@ let 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; }; }; @@ -517,6 +527,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; }; }; @@ -681,6 +698,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 = '' @@ -712,6 +735,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 = '' diff --git a/tests/homebrew.nix b/tests/homebrew.nix index 7815158..beaf5ff 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -50,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 = [ @@ -62,6 +66,10 @@ in name = "opera"; greedy = true; } + { + name = "google-cloud-sdk"; + postinstall = "\${HOMEBREW_PREFIX}/bin/gcloud components update"; + } ]; homebrew.masApps = { @@ -92,11 +100,13 @@ in ${mkTest "imagemagick" ''brew "imagemagick"''} ${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''} From cbe4a600d4f48c2409888c8131e5cb42d6206e6d Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 21:01:07 -0800 Subject: [PATCH 06/16] modules/homebrew: add `homebrew.goPackages` option Add support for `go "pkg"` entries in the generated Brewfile. Homebrew Bundle supports installing Go packages via `go install`; the `go` formula is automatically installed if not already present. --- modules/homebrew.nix | 25 ++++++++++++++++++++----- tests/homebrew.nix | 7 +++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index aadec07..f41877d 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -562,16 +562,18 @@ in 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, Go packages, and Docker containers, 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.goPackages), + [](#opt-homebrew.whalebrews), and [](#opt-homebrew.vscode) options to list the + Homebrew formulae, casks, Mac App Store apps, Go packages, 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.caskArgs) options) to generate a Brewfile that {command}`nix-darwin` passes to the {command}`brew bundle` command during system activation. @@ -775,6 +777,18 @@ 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. + ''; + }; + whalebrews = mkOption { type = with types; listOf str; default = [ ]; @@ -852,6 +866,7 @@ in + mkBrewfileSectionString "Casks" cfg.casks + mkBrewfileSectionString "Mac App Store apps" (mapAttrsToList (n: id: ''mas "${n}", id: ${toString id}'') cfg.masApps) + + mkBrewfileSectionString "Go packages" (map (v: ''go "${v}"'') cfg.goPackages) + mkBrewfileSectionString "Docker containers" (map (v: ''whalebrew "${v}"'') cfg.whalebrews) + mkBrewfileSectionString "Visual Studio Code extensions" (map (v: ''vscode "${v}"'') cfg.vscode) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); diff --git a/tests/homebrew.nix b/tests/homebrew.nix index beaf5ff..1f83d59 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -77,6 +77,10 @@ in Xcode = 497799835; }; + homebrew.goPackages = [ + "github.com/charmbracelet/crush" + ]; + homebrew.whalebrews = [ "whalebrew/wget" ]; @@ -112,6 +116,9 @@ in ${mkTest "1Password for Safari" ''mas "1Password for Safari", id: 1569813296''} ${mkTest "Xcode" ''mas "Xcode", id: 497799835''} + echo "checking go entries in Brewfile" >&2 + ${mkTest "github.com/charmbracelet/crush" ''go "github.com/charmbracelet/crush"''} + echo "checking whalebrew entries in Brewfile" >&2 ${mkTest "whalebrew/wget" ''whalebrew "whalebrew/wget"''} From 3479b795aa60c641a641bab11172b57f3c6ff9a6 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 21:04:53 -0800 Subject: [PATCH 07/16] modules/homebrew: add `homebrew.cargoPackages` option Add support for `cargo "pkg"` entries in the generated Brewfile. Homebrew Bundle supports installing Rust crates via `cargo install`; the `rust` formula is automatically installed if not already present. --- modules/homebrew.nix | 23 +++++++++++++++++++---- tests/homebrew.nix | 7 +++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index f41877d..a47d677 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -562,16 +562,18 @@ in options.homebrew = { enable = mkEnableOption '' {command}`nix-darwin` to manage installing/updating/upgrading Homebrew taps, formulae, - casks, Mac App Store apps, Go packages, and Docker containers, using Homebrew Bundle. + casks, Mac App Store apps, Go packages, Cargo crates, and Docker containers, 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.goPackages), - [](#opt-homebrew.whalebrews), and [](#opt-homebrew.vscode) options to list the - Homebrew formulae, casks, Mac App Store apps, Go packages, Docker containers, - and Visual Studio Code extensions you'd like to install. Use the + [](#opt-homebrew.cargoPackages), [](#opt-homebrew.whalebrews), and + [](#opt-homebrew.vscode) options to list the Homebrew formulae, casks, Mac App + Store apps, Go packages, Cargo crates, 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.caskArgs) options) to generate a Brewfile that @@ -789,6 +791,18 @@ in ''; }; + 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. + ''; + }; + whalebrews = mkOption { type = with types; listOf str; default = [ ]; @@ -867,6 +881,7 @@ in + mkBrewfileSectionString "Mac App Store apps" (mapAttrsToList (n: id: ''mas "${n}", id: ${toString id}'') cfg.masApps) + mkBrewfileSectionString "Go packages" (map (v: ''go "${v}"'') cfg.goPackages) + + mkBrewfileSectionString "Cargo packages" (map (v: ''cargo "${v}"'') cfg.cargoPackages) + mkBrewfileSectionString "Docker containers" (map (v: ''whalebrew "${v}"'') cfg.whalebrews) + mkBrewfileSectionString "Visual Studio Code extensions" (map (v: ''vscode "${v}"'') cfg.vscode) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); diff --git a/tests/homebrew.nix b/tests/homebrew.nix index 1f83d59..b87a176 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -81,6 +81,10 @@ in "github.com/charmbracelet/crush" ]; + homebrew.cargoPackages = [ + "ripgrep" + ]; + homebrew.whalebrews = [ "whalebrew/wget" ]; @@ -119,6 +123,9 @@ in 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 whalebrew entries in Brewfile" >&2 ${mkTest "whalebrew/wget" ''whalebrew "whalebrew/wget"''} From 65cfcebaa2ab8504744137c65ebe336e7fe4053b Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 21:22:06 -0800 Subject: [PATCH 08/16] modules/homebrew: remove `homebrew.whalebrews` option Whalebrew support was fully removed from Homebrew Bundle in Homebrew 4.7.0 (Nov 2025). A `whalebrew` entry in a Brewfile now raises `RuntimeError: Invalid Brewfile: undefined method 'whalebrew'`, breaking the entire `brew bundle` invocation. Use `mkRemovedOptionModule` so that existing configs get a clear warning instead of an undefined-option error. Also removes the auto-addition of `"whalebrew"` to `homebrew.brews` and the Brewfile generation for Docker containers. --- modules/homebrew.nix | 31 ++++++------------------------- tests/homebrew.nix | 8 +------- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index a47d677..ab33b10 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -557,23 +557,22 @@ in imports = [ (mkRenamedOptionModule [ "homebrew" "autoUpdate" ] [ "homebrew" "onActivation" "autoUpdate" ]) (mkRenamedOptionModule [ "homebrew" "cleanup" ] [ "homebrew" "onActivation" "cleanup" ]) + (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, - casks, Mac App Store apps, Go packages, Cargo crates, and Docker containers, using Homebrew - Bundle. + casks, Mac App Store apps, 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.goPackages), - [](#opt-homebrew.cargoPackages), [](#opt-homebrew.whalebrews), and - [](#opt-homebrew.vscode) options to list the Homebrew formulae, casks, Mac App - Store apps, Go packages, Cargo crates, Docker containers, and Visual Studio Code - extensions you'd like to install. Use the + [](#opt-homebrew.cargoPackages), and [](#opt-homebrew.vscode) options to list the + Homebrew formulae, casks, Mac App Store apps, Go packages, Cargo crates, 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.caskArgs) options) to generate a Brewfile that @@ -803,21 +802,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 = [ ]; @@ -834,6 +818,7 @@ in ''; }; + extraConfig = mkOption { type = types.lines; default = ""; @@ -868,9 +853,6 @@ in "homebrew.enable" ]; - homebrew.brews = - optional (cfg.whalebrews != [ ]) "whalebrew"; - homebrew.brewfile = "# Created by `nix-darwin`'s `homebrew` module\n\n" + mkBrewfileSectionString "Taps" cfg.taps @@ -882,7 +864,6 @@ in (mapAttrsToList (n: id: ''mas "${n}", id: ${toString id}'') cfg.masApps) + mkBrewfileSectionString "Go packages" (map (v: ''go "${v}"'') cfg.goPackages) + mkBrewfileSectionString "Cargo packages" (map (v: ''cargo "${v}"'') cfg.cargoPackages) - + mkBrewfileSectionString "Docker containers" (map (v: ''whalebrew "${v}"'') cfg.whalebrews) + mkBrewfileSectionString "Visual Studio Code extensions" (map (v: ''vscode "${v}"'') cfg.vscode) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); diff --git a/tests/homebrew.nix b/tests/homebrew.nix index b87a176..ed9a5fd 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -85,14 +85,11 @@ in "ripgrep" ]; - homebrew.whalebrews = [ - "whalebrew/wget" - ]; - homebrew.vscode = [ "golang.go" ]; + test = '' bf=${lib.escapeShellArg config.homebrew.brewfile} @@ -126,9 +123,6 @@ in echo "checking cargo entries in Brewfile" >&2 ${mkTest "ripgrep" ''cargo "ripgrep"''} - 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"''} ''; From 24531016d84690edfab9f9a8978df4976f6c2b98 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 21:39:43 -0800 Subject: [PATCH 09/16] modules/homebrew: deprecate `homebrew.global.lockfiles` Homebrew Bundle removed lockfile support in Homebrew 4.4.0 (Oct 2024): the `--no-lock` CLI flag, the `HOMEBREW_BUNDLE_NO_LOCK` env var, and the `no_lock` parameter in `installer.rb` are all dead code. Setting `homebrew.global.lockfiles` has had no effect on current Homebrew versions. - Replace the `lockfiles` option with a hidden stub (matching `noLock`) - Replace the `noLock` hard assertion with a shared deprecation warning for both options - Stop setting `HOMEBREW_BUNDLE_NO_LOCK` in `environment.variables` - Remove the lockfiles paragraph from the `brewfile` option description --- modules/homebrew.nix | 47 ++++++++------------------------------------ 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index ab33b10..6acd4a5 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -164,14 +164,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 @@ -200,31 +192,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; }; @@ -234,7 +208,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"; }; }; }; @@ -840,13 +813,9 @@ 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.") ]; system.requiresPrimaryUser = mkIf (cfg.enable && options.homebrew.user.highestPrio == (mkOptionDefault {}).priority) [ From 8c29e146dd2dcef4ff427978e5d3b12add8bed67 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 9 Feb 2026 23:07:06 -0800 Subject: [PATCH 10/16] modules/homebrew: replace `brewPrefix` with `prefix` Closes #596 `homebrew.brewPrefix` defaulted to the bin directory (`/opt/homebrew/bin`), not the actual Homebrew prefix (`/opt/homebrew`). This misled users into writing `${config.homebrew.brewPrefix}/bin`, producing the broken path `/opt/homebrew/bin/bin`. Replace it with `homebrew.prefix`, which has correct semantics matching `brew --prefix`. The old `brewPrefix` option is removed using `mkRemovedOptionModule`, which catches both users who set the option and users who read it in custom code. A warning also fires if the new `prefix` value ends with `/bin`, catching users who copy the old value verbatim. --- modules/homebrew.nix | 20 +++++++++++--------- modules/system/checks.nix | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 6acd4a5..788a041 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -530,6 +530,7 @@ 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.") ]; @@ -572,17 +573,17 @@ 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. ''; }; @@ -816,6 +817,7 @@ in 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) [ @@ -841,8 +843,8 @@ in 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} \ 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 From ca6f8609c37d14e1018f7f02bf17aca06c8cae1c Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 10 Feb 2026 00:06:24 -0800 Subject: [PATCH 11/16] 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) ''; } From c68f5d13873e94f4b27130ad1dca74dfd19a01e4 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 10 Feb 2026 01:46:53 -0800 Subject: [PATCH 12/16] 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) ''; } From e0ffd55e7a45592118d721efaef94d9090048487 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 10 Feb 2026 03:26:27 -0800 Subject: [PATCH 13/16] modules/homebrew: fix typos and improve option descriptions --- modules/homebrew.nix | 97 ++++++++++++++++++++++++++++---------------- tests/homebrew.nix | 14 +++---- 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 04c94bb..10047b0 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -118,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 @@ -191,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 @@ -247,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. ''; }; @@ -379,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; }; @@ -418,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 `"--"`. ''; }; @@ -436,8 +455,9 @@ let 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. If set to `"always"`, the service will - be restarted on every {command}`brew bundle` run, even if nothing changed. + 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`. ''; @@ -445,7 +465,9 @@ 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`. ''; @@ -456,7 +478,7 @@ let description = '' 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 + 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. ''; @@ -549,16 +571,17 @@ in options.homebrew = { enable = mkEnableOption '' {command}`nix-darwin` to manage installing/updating/upgrading Homebrew taps, formulae, - casks, Mac App Store apps, Go packages, and Cargo crates 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.goPackages), - [](#opt-homebrew.cargoPackages), and [](#opt-homebrew.vscode) options to list the - Homebrew formulae, casks, Mac App Store apps, Go packages, Cargo crates, and - Visual Studio Code extensions you'd like to install. Use 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 @@ -668,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`. ''; }; @@ -771,6 +796,22 @@ in ''; }; + vscode = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "golang.go" ]; + description = '' + List of Visual Studio Code extensions to install using Homebrew Bundle. + + A compatible editor (Visual Studio Code, VSCodium, Cursor, or VS Code Insiders) + must be available. If none is found, Homebrew will attempt to install + `visual-studio-code` automatically. + + For more information on {command}`code` see: + [VSCode Extension Marketplace](https://code.visualstudio.com/docs/editor/extension-marketplace). + ''; + }; + goPackages = mkOption { type = with types; listOf str; default = [ ]; @@ -795,22 +836,6 @@ in ''; }; - vscode = mkOption { - type = with types; listOf str; - default = [ ]; - example = [ "golang.go" ]; - description = '' - List of Visual Studio Code extensions to install using Homebrew Bundle. - - A compatible editor (Visual Studio Code, VSCodium, Cursor, or VS Code Insiders) - must be available. If none is found, Homebrew will attempt to install - `visual-studio-code` automatically. - - For more information on {command}`code` see: - [VSCode Extension Marketplace](https://code.visualstudio.com/docs/editor/extension-marketplace). - ''; - }; - extraConfig = mkOption { type = types.lines; @@ -824,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."; }; }; @@ -852,9 +877,9 @@ in + mkBrewfileSectionString "Casks" cfg.casks + mkBrewfileSectionString "Mac App Store apps" (mapAttrsToList (n: id: ''mas "${n}", id: ${toString id}'') cfg.masApps) + + 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) - + mkBrewfileSectionString "Visual Studio Code extensions" (map (v: ''vscode "${v}"'') cfg.vscode) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); environment.variables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables; @@ -929,4 +954,8 @@ in fi ''; }; + + meta.maintainers = [ + lib.maintainers.malo or "malo" + ]; } diff --git a/tests/homebrew.nix b/tests/homebrew.nix index 1552010..e026197 100644 --- a/tests/homebrew.nix +++ b/tests/homebrew.nix @@ -77,6 +77,10 @@ in Xcode = 497799835; }; + homebrew.vscode = [ + "golang.go" + ]; + homebrew.goPackages = [ "github.com/charmbracelet/crush" ]; @@ -85,10 +89,6 @@ in "ripgrep" ]; - homebrew.vscode = [ - "golang.go" - ]; - test = '' bf=${lib.escapeShellArg config.homebrew.brewfile} @@ -117,15 +117,15 @@ in ${mkTest "1Password for Safari" ''mas "1Password for Safari", id: 1569813296''} ${mkTest "Xcode" ''mas "Xcode", id: 497799835''} + 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 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) From a43b4091dbd66140855930472ae9c49e06626afa Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 10 Feb 2026 11:38:39 -0800 Subject: [PATCH 14/16] modules/homebrew: add CHANGELOG entry for module refresh --- CHANGELOG | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) 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 From 6d789c5a41dc9db21bba259dbeff99964d87e0e8 Mon Sep 17 00:00:00 2001 From: Michael Hoang Date: Tue, 17 Feb 2026 23:36:16 +0100 Subject: [PATCH 15/16] etc: support experimental official Nix installer --- ...11f4ba5f9d5f8de1c9f5efb5c607de88faf5f58b8b9cb38edbf | 7 +++++++ ...55ca0f2e6f85137037d660b3224a34d59305e8530ca292bc734 | 3 +++ ...1b101617685fd3d001f74a9466d9d763d92eb75b99cc740db91 | 10 ++++++++++ modules/nix/default.nix | 7 +++++-- modules/programs/zsh/default.nix | 1 + 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 doc/known-files/4e8f7cb9b699511f4ba5f9d5f8de1c9f5efb5c607de88faf5f58b8b9cb38edbf create mode 100644 doc/known-files/71f7fdc9f6c9e55ca0f2e6f85137037d660b3224a34d59305e8530ca292bc734 create mode 100644 doc/known-files/74ee0ae5ad21a1b101617685fd3d001f74a9466d9d763d92eb75b99cc740db91 diff --git a/doc/known-files/4e8f7cb9b699511f4ba5f9d5f8de1c9f5efb5c607de88faf5f58b8b9cb38edbf b/doc/known-files/4e8f7cb9b699511f4ba5f9d5f8de1c9f5efb5c607de88faf5f58b8b9cb38edbf new file mode 100644 index 0000000..d9e7156 --- /dev/null +++ b/doc/known-files/4e8f7cb9b699511f4ba5f9d5f8de1c9f5efb5c607de88faf5f58b8b9cb38edbf @@ -0,0 +1,7 @@ + +# Set up Nix only on SSH connections +# See: https://github.com/DeterminateSystems/nix-installer/pull/714 +if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ] && [ -n "${SSH_CONNECTION:-}" ] && [ "${SHLVL:-0}" -eq 1 ]; then + . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' +fi +# End Nix diff --git a/doc/known-files/71f7fdc9f6c9e55ca0f2e6f85137037d660b3224a34d59305e8530ca292bc734 b/doc/known-files/71f7fdc9f6c9e55ca0f2e6f85137037d660b3224a34d59305e8530ca292bc734 new file mode 100644 index 0000000..f0789e1 --- /dev/null +++ b/doc/known-files/71f7fdc9f6c9e55ca0f2e6f85137037d660b3224a34d59305e8530ca292bc734 @@ -0,0 +1,3 @@ +# Written by https://github.com/NixOS/nix-installer +# The contents below are based on options specified at installation time. + diff --git a/doc/known-files/74ee0ae5ad21a1b101617685fd3d001f74a9466d9d763d92eb75b99cc740db91 b/doc/known-files/74ee0ae5ad21a1b101617685fd3d001f74a9466d9d763d92eb75b99cc740db91 new file mode 100644 index 0000000..1df4ff6 --- /dev/null +++ b/doc/known-files/74ee0ae5ad21a1b101617685fd3d001f74a9466d9d763d92eb75b99cc740db91 @@ -0,0 +1,10 @@ +# Generated by https://github.com/NixOS/nix-installer +# See `/nix/nix-installer --version` for the version details. + +extra-experimental-features = nix-command flakes +always-allow-substitutes = true +bash-prompt-prefix = (nix:$name)\040 +max-jobs = auto +extra-nix-path = nixpkgs=flake:nixpkgs + +!include nix.custom.conf diff --git a/modules/nix/default.nix b/modules/nix/default.nix index 00e62d5..724b98a 100644 --- a/modules/nix/default.nix +++ b/modules/nix/default.nix @@ -757,6 +757,7 @@ in "6bb8d6b0dd16b44ee793a9b8382dac76c926e4c16ffb8ddd2bb4884d1ca3f811" # DeterminateSystems Nix installer 0.34.0 "24797ac05542ff8b52910efc77870faa5f9e3275097227ea4e50c430a5f72916" # lix-installer 0.17.1 with flakes "b027b5cad320b5b8123d9d0db9f815c3f3921596c26dc3c471457098e4d3cc40" # lix-installer 0.17.1 without flakes + "74ee0ae5ad21a1b101617685fd3d001f74a9466d9d763d92eb75b99cc740db91" # experimental official Nix installer 2.33.3 ]; environment.etc."nix/registry.json".text = builtins.toJSON { @@ -881,10 +882,12 @@ in # to express that we want it deleted and know only one hash? system.activationScripts.checks.text = mkAfter '' nixCustomConfKnownSha256Hashes=( - # v0.33.0 + # DetSys v0.33.0 6787fade1cf934f82db554e78e1fc788705c2c5257fddf9b59bdd963ca6fec63 - # v0.34.0 + # DetSys v0.34.0 3bd68ef979a42070a44f8d82c205cfd8e8cca425d91253ec2c10a88179bb34aa + # Nix 2.33.3 + 71f7fdc9f6c9e55ca0f2e6f85137037d660b3224a34d59305e8530ca292bc734 ) if [[ -e /etc/nix/nix.custom.conf ]]; then nixCustomConfSha256Output=$(shasum -a 256 /etc/nix/nix.custom.conf) diff --git a/modules/programs/zsh/default.nix b/modules/programs/zsh/default.nix index c5de334..94333f4 100644 --- a/modules/programs/zsh/default.nix +++ b/modules/programs/zsh/default.nix @@ -260,6 +260,7 @@ in environment.etc."zshenv".knownSha256Hashes = [ "d07015be6875f134976fce84c6c7a77b512079c1c5f9594dfa65c70b7968b65f" # DeterminateSystems installer + "4e8f7cb9b699511f4ba5f9d5f8de1c9f5efb5c607de88faf5f58b8b9cb38edbf" # experimental official Nix installer 2.33.3 ]; }; From ebe39ab3fa05d9637c2ac6166b9d8b5c3047f60b Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Tue, 17 Feb 2026 18:03:42 -0800 Subject: [PATCH 16/16] readme: Use bullet-list for documentation location This will make it much easier to find the online documentation at a quick glance, as well as making it easier to see the commands for viewing locally. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3507254..8c99e54 100644 --- a/README.md +++ b/README.md @@ -166,9 +166,11 @@ sudo nix-channel --update ## Documentation -`darwin-help` will open up a local copy of the reference documentation, it can also be found online [here](https://nix-darwin.github.io/nix-darwin/manual/index.html). +The reference documentation is available: -The documentation is also available as manpages by running `man 5 configuration.nix`. +* Online: [nix-darwin reference](https://nix-darwin.github.io/nix-darwin/manual/index.html) +* Locally in your browser via the `darwin-help` command +* As a manual page via `man 5 configuration.nix` ## Uninstalling