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}