diff --git a/CHANGELOG b/CHANGELOG
index 05c5f44d9..1675b3e0f 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,6 @@
+2021-01-16
+- Added `homebrew` module, to manage formulas installed by Homebrew via `brew bundle`.
+
2020-10-25
- The option environment.variables.SHELL is no longer set automatically when,
eg. programs.zsh.enable is configured.
diff --git a/modules/homebrew.nix b/modules/homebrew.nix
new file mode 100644
index 000000000..d67f5b2a0
--- /dev/null
+++ b/modules/homebrew.nix
@@ -0,0 +1,214 @@
+# Created by: https://github.com/malob
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.homebrew;
+
+ brewfileSection = heading: type: entries: optionalString (entries != [])
+ "# ${heading}\n" + (concatMapStrings (name: "${type} \"${name}\"\n") entries) + "\n";
+
+ masBrewfileSection = entries: optionalString (entries != {}) (
+ "# Mac App Store apps\n" +
+ concatStringsSep "\n" (mapAttrsToList (name: id: ''mas "${name}", id: ${toString id}'') entries) +
+ "\n"
+ );
+
+ brewfile = pkgs.writeText "Brewfile" (
+ brewfileSection "Taps" "tap" cfg.taps +
+ brewfileSection "Brews" "brew" cfg.brews +
+ brewfileSection "Casks" "cask" cfg.casks +
+ masBrewfileSection cfg.masApps +
+ brewfileSection "Docker containers" "whalebrew" cfg.whalebrews +
+ optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig)
+ );
+
+ brew-bundle-command = concatStringsSep " " (
+ optional (!cfg.autoUpdate) "HOMEBREW_NO_AUTO_UPDATE=1" ++
+ [ "brew bundle --file='${brewfile}' --no-lock" ] ++
+ optional (cfg.cleanup == "uninstall" || cfg.cleanup == "zap") "--cleanup" ++
+ optional (cfg.cleanup == "zap") "--zap"
+ );
+in
+
+{
+ options.homebrew = {
+ enable = mkEnableOption ''
+ configuring your Brewfile, and installing/updating the formulas therein via
+ the brew bundle command, using nix-darwin.
+
+ Note that enabling this option does not install Homebrew. See the Homebrew website for
+ installation instructions: https://brew.sh
+ '';
+
+ autoUpdate = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When enabled, Homebrew is allowed to auto-update during nix-darwin
+ activation. The default is false so that repeated invocations of
+ darwin-rebuild switch are idempotent.
+ '';
+ };
+
+ cleanup = mkOption {
+ type = types.enum [ "none" "uninstall" "zap" ];
+ default = "none";
+ example = "uninstall";
+ description = ''
+ This option manages what happens to formulas installed by Homebrew, that aren't present in
+ the Brewfile generated by this module.
+
+ When set to "none" (the default), formulas not present in the generated
+ Brewfile are left installed.
+
+ When set to "uninstall", nix-darwin invokes
+ brew bundle [install] with the --cleanup flag. This
+ uninstalls all formulas not listed in generate Brewfile, i.e.,
+ brew uninstall is run for those formulas.
+
+ When set to "zap", nix-darwin invokes
+ brew bundle [install] with the --cleanup --zap
+ flags. This uninstalls all formulas not listed in the generated Brewfile, and if the
+ formula is a cask, removes all files associated with that cask. In other words,
+ brew uninstall --zap is run for all those formulas.
+
+ If you plan on exclusively using nix-darwin to manage formulas installed
+ by Homebrew, you probably want to set this option to "uninstall" or
+ "zap".
+ '';
+ };
+
+ global.brewfile = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When enabled, when you manually invoke brew bundle, it will automatically
+ use the Brewfile in the Nix store that this module generates.
+
+ Sets the HOMEBREW_BUNDLE_FILE environment variable to the path of the
+ Brewfile in the Nix store that this module generates, by adding it to
+ .
+ '';
+ };
+
+ global.noLock = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ When enabled, lockfiles aren't generated when you manually invoke
+ brew bundle [install]. This is often desirable when
+ is enabled, since
+ brew bundle [install] will try to write the lockfile in the Nix store,
+ and complain that it can't (though the command will run successfully regardless).
+
+ Sets the HOMEBREW_BUNDLE_NO_LOCK environment variable, by adding it to
+ .
+ '';
+ };
+
+ taps = mkOption {
+ type = with types; listOf str;
+ default = [];
+ example = [ "homebrew/cask-fonts" ];
+ description = "Homebrew formula repositories to tap.";
+ };
+
+ brews = mkOption {
+ type = with types; listOf str;
+ default = [];
+ example = [ "mas" ];
+ description = "Homebrew brews to install.";
+ };
+
+ casks = mkOption {
+ type = with types; listOf str;
+ default = [];
+ example = [ "hammerspoon" "virtualbox" ];
+ description = "Homebrew casks to install.";
+ };
+
+ masApps = mkOption {
+ type = with types; attrsOf ints.positive;
+ default = {};
+ example = {
+ "1Password" = 1107421413;
+ Xcode = 497799835;
+ };
+ description = ''
+ Applications to install from Mac App Store using mas.
+
+ When this option is used, "mas" is automatically added to
+ .
+
+ Note that you need to be signed into the Mac App Store for mas to
+ successfully install and upgrade applications, and that unfortunately apps removed from this
+ option will not be uninstalled automatically even if
+ is set to "uninstall"
+ or "zap" (this is currently a limitation of Homebrew Bundle).
+
+ For more information on mas see: https://github.com/mas-cli/mas.
+ '';
+ };
+
+ whalebrews = mkOption {
+ type = with types; listOf str;
+ default = [];
+ example = [ "whalebrew/wget" ];
+ description = ''
+ Docker images to install using whalebrew.
+
+ When this option is used, "whalebrew" is automatically added to
+ .
+
+ For more information on whalebrew see:
+ https://github.com/whalebrew/whalebrew.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ example = ''
+ # 'brew tap' with custom Git URL
+ tap "user/tap-repo", "https://user@bitbucket.org/user/homebrew-tap-repo.git"
+
+ # set arguments for all 'brew cask install' commands
+ cask_args appdir: "~/Applications", require_sha: true
+
+ # 'brew install --with-rmtp', 'brew services restart' on version changes
+ brew "denji/nginx/nginx-full", args: ["with-rmtp"], restart_service: :changed
+ # 'brew install', always 'brew services restart', 'brew link', 'brew unlink mysql' (if it is installed)
+ brew "mysql@5.6", restart_service: true, link: true, conflicts_with: ["mysql"]
+
+ # 'brew cask install --appdir=~/my-apps/Applications'
+ cask "firefox", args: { appdir: "~/my-apps/Applications" }
+ # 'brew cask install' only if '/usr/libexec/java_home --failfast' fails
+ cask "java" unless system "/usr/libexec/java_home --failfast"
+ '';
+ description = "Extra lines to be added verbatim to the generated Brewfile.";
+ };
+ };
+
+ config = {
+ homebrew.brews =
+ optional (cfg.masApps != {}) "mas" ++
+ optional (cfg.whalebrews != []) "whalebrew";
+
+ environment.variables = mkIf cfg.enable (
+ optionalAttrs cfg.global.brewfile { HOMEBREW_BUNDLE_FILE = "${brewfile}"; } //
+ optionalAttrs cfg.global.noLock { HOMEBREW_BUNDLE_NO_LOCK = "1"; }
+ );
+
+ system.activationScripts.homebrew.text = mkIf cfg.enable ''
+ # Homebrew Bundle
+ echo >&2 "Homebrew bundle..."
+ if [ -f /usr/local/bin/brew ]; then
+ PATH=/usr/local/bin:$PATH ${brew-bundle-command}
+ else
+ echo -e "\e[1;31merror: Homebrew is not installed, skipping...\e[0m" >&2
+ fi
+ '';
+ };
+}
diff --git a/modules/module-list.nix b/modules/module-list.nix
index d7ba55091..447d07576 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -68,6 +68,7 @@
./programs/tmux.nix
./programs/vim.nix
./programs/zsh
+ ./homebrew.nix
./users
./users/nixbld
]
diff --git a/modules/system/activation-scripts.nix b/modules/system/activation-scripts.nix
index a9aac25be..346fb97ce 100644
--- a/modules/system/activation-scripts.nix
+++ b/modules/system/activation-scripts.nix
@@ -105,6 +105,7 @@ in
${cfg.activationScripts.extraUserActivation.text}
${cfg.activationScripts.userDefaults.text}
${cfg.activationScripts.userLaunchd.text}
+ ${cfg.activationScripts.homebrew.text}
${cfg.activationScripts.postUserActivation.text}