From 92da7697d19d72a58bd672a511c48f69f45606cc Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 22 Aug 2022 14:49:22 -0700 Subject: [PATCH 01/10] Add `homebrew.caskArgs` option --- modules/homebrew.nix | 166 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 5 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index e507557..6a28bb7 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -16,12 +16,13 @@ let ); brewfile = pkgs.writeText "Brewfile" ( - optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig) + brewfileSection "Taps" "tap" cfg.taps + + (if cfg.caskArgs.brewfileLine == null then "" else "# Arguments for all casks\n${cfg.caskArgs.brewfileLine}\n\n") + brewfileSection "Brews" "brew" cfg.brews + brewfileSection "Casks" "cask" cfg.casks + masBrewfileSection cfg.masApps + - brewfileSection "Docker containers" "whalebrew" cfg.whalebrews + brewfileSection "Docker containers" "whalebrew" cfg.whalebrews + + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig) ); brew-bundle-command = concatStringsSep " " ( @@ -30,6 +31,154 @@ let optional (cfg.cleanup == "uninstall" || cfg.cleanup == "zap") "--cleanup" ++ optional (cfg.cleanup == "zap") "--zap" ); + + mkBrewfileLineValueString = v: + if isInt v then toString v + else if isFloat v then strings.floatToString v + else if isBool v then boolToString v + else if isString v then ''"${v}"'' + else if isAttrs v then "{ ${concatStringsSep ", " (mapAttrsToList (n: v': "${n}: ${mkBrewfileLineValueString v'}") v)} }" + else if isList v then "[${concatMapStringsSep ", " mkBrewfileLineValueString v}]" + else abort "The value: ${generators.toPretty v} is not a valid Brewfile value."; + + mkBrewfileLineOptionsListString = attrs: + concatStringsSep ", " (mapAttrsToList (n: v: "${n}: ${mkBrewfileLineValueString v}") attrs); + + mkNullOrBoolOption = args: mkOption (args // { + type = types.nullOr types.bool; + default = null; + }); + + mkNullOrStrOption = args: mkOption (args // { + type = types.nullOr types.str; + default = null; + }); + + # Sourced from https://docs.brew.sh/Manpage#global-cask-options + # and valid values for `HOMEBREW_CASK_OPTS`. + caskArgsOptions = { config, ... }: { + options = { + appdir = mkNullOrStrOption { + description = '' + Target location for Applications + (default: /Applications) + ''; + }; + colorpickerdir = mkNullOrStrOption { + description = '' + Target location for Color Pickers + (default: ~/Library/ColorPickers) + ''; + }; + prefpanedir = mkNullOrStrOption { + description = '' + Target location for Preference Panes + (default: ~/Library/PreferencePanes) + ''; + }; + qlplugindir = mkNullOrStrOption { + description = '' + Target location for QuickLook Plugins + (default: ~/Library/QuickLook) + ''; + }; + mdimporterdir = mkNullOrStrOption { + description = '' + Target location for Spotlight Plugins + (default: ~/Library/Spotlight) + ''; + }; + dictionarydir = mkNullOrStrOption { + description = '' + Target location for Dictionaries + (default: ~/Library/Dictionaries) + ''; + }; + fontdir = mkNullOrStrOption { + description = '' + Target location for Fonts + (default: ~/Library/Fonts) + ''; + }; + servicedir = mkNullOrStrOption { + description = '' + Target location for Services + (default: ~/Library/Services) + ''; + }; + input_methoddir = mkNullOrStrOption { + description = '' + Target location for Input Methods + (default: ~/Library/Input Methods) + ''; + }; + internet_plugindir = mkNullOrStrOption { + description = '' + Target location for Internet Plugins + (default: ~/Library/Internet Plug-Ins) + ''; + }; + audio_unit_plugindir = mkNullOrStrOption { + description = '' + Target location for Audio Unit Plugins + (default: ~/Library/Audio/Plug-Ins/Components) + ''; + }; + vst_plugindir = mkNullOrStrOption { + description = '' + Target location for VST Plugins + (default: ~/Library/Audio/Plug-Ins/VST) + ''; + }; + vst3_plugindir = mkNullOrStrOption { + description = '' + Target location for VST3 Plugins + (default: ~/Library/Audio/Plug-Ins/VST3) + ''; + }; + screen_saverdir = mkNullOrStrOption { + description = '' + Target location for Screen Savers + (default: ~/Library/Screen Savers) + ''; + }; + language = mkNullOrStrOption { + description = '' + Comma-separated list of language codes to prefer for cask installation. The first matching + language is used, otherwise it reverts to the cask’s default language. The default value + is the language of your system. + ''; + example = "zh-TW"; + }; + require_sha = mkNullOrBoolOption { + description = "Whether to require cask(s) to have a checksum."; + }; + no_quarantine = mkNullOrBoolOption { + description = "Whether to disable quarantining of downloads."; + }; + no_binaries = mkNullOrBoolOption { + description = "Whether to disable linking of helper executables."; + }; + + brewfileLine = mkOption { + type = types.nullOr types.str; + visible = false; + internal = true; + readOnly = true; + }; + }; + + config = + let + configuredOptions = filterAttrs (_: v: v != null) (removeAttrs config [ "_module" "brewfileLine" ]); + in + { + brewfileLine = + if configuredOptions == {} + then null + else "cask_args " + mkBrewfileLineOptionsListString configuredOptions; + }; + }; in { @@ -130,6 +279,16 @@ in description = "Homebrew brews to install."; }; + caskArgs = mkOption { + type = types.submodule caskArgsOptions; + default = {}; + example = { + appdir = "~/Applications"; + require_sha = true; + }; + description = "Arguments to apply to all ."; + }; + casks = mkOption { type = with types; listOf str; default = []; @@ -182,9 +341,6 @@ in # '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) From bd93329d6c9b28178e551b9f92538097209d1138 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 22 Aug 2022 18:10:46 -0700 Subject: [PATCH 02/10] Enable defining options for `taps` using submodule --- modules/homebrew.nix | 107 +++++++++++++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 24 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 6a28bb7..355061b 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -6,22 +6,27 @@ with lib; let cfg = config.homebrew; - brewfileSection = heading: type: entries: optionalString (entries != []) - "# ${heading}\n" + (concatMapStrings (name: "${type} \"${name}\"\n") entries) + "\n"; + mkBrewfileSectionString = heading: type: entries: optionalString (entries != []) '' + # ${heading} + ${concatMapStringsSep "\n" (v: v.brewfileLine or ''${type} "${v}"'') entries} - masBrewfileSection = entries: optionalString (entries != {}) ( + ''; + + mkMasBrewfileSectionString = 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 + - (if cfg.caskArgs.brewfileLine == null then "" else "# Arguments for all casks\n${cfg.caskArgs.brewfileLine}\n\n") + - brewfileSection "Brews" "brew" cfg.brews + - brewfileSection "Casks" "cask" cfg.casks + - masBrewfileSection cfg.masApps + - brewfileSection "Docker containers" "whalebrew" cfg.whalebrews + + mkBrewfileSectionString "Taps" "tap" cfg.taps + + mkBrewfileSectionString "Arguments for all casks" "cask_args" + (optional (cfg.caskArgs.brewfileLine != null) cfg.caskArgs) + + mkBrewfileSectionString "Brews" "brew" cfg.brews + + mkBrewfileSectionString "Casks" "cask" cfg.casks + + mkMasBrewfileSectionString cfg.masApps + + mkBrewfileSectionString "Docker containers" "whalebrew" cfg.whalebrews + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig) ); @@ -54,6 +59,50 @@ let default = null; }); + mkBrewfileLineOption = mkOption { + type = types.nullOr types.str; + visible = false; + internal = true; + readOnly = true; + }; + + tapOptions = { config, ... }: { + options = { + name = mkOption { + type = types.str; + example = "homebrew/cask-fonts"; + description = '' + When is unspecified, this is the name of a formula + repository to tap from GitHub using HTTPS. For example, "user/repo" will + tap https://github.com/user/homebrew-repo. + ''; + }; + clone_target = mkNullOrStrOption { + description = '' + Use this option to tap a formula repository from anywhere, using any transport protocol + that git handles. When is specified, taps + can be cloned from places other than GitHub and using protocols other than HTTPS, e.g., + SSH, git, HTTP, FTP(S), rsync. + ''; + }; + force_auto_update = mkNullOrBoolOption { + 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). + ''; + }; + + brewfileLine = mkBrewfileLineOption; + }; + + config = { + brewfileLine = ''tap "${config.name}"'' + + optionalString (config.clone_target != null) '', "${config.clone_target}"'' + + optionalString (config.force_auto_update != null) + ", force_auto_update: ${boolToString config.force_auto_update}"; + }; + }; + # Sourced from https://docs.brew.sh/Manpage#global-cask-options # and valid values for `HOMEBREW_CASK_OPTS`. caskArgsOptions = { config, ... }: { @@ -160,22 +209,17 @@ let description = "Whether to disable linking of helper executables."; }; - brewfileLine = mkOption { - type = types.nullOr types.str; - visible = false; - internal = true; - readOnly = true; - }; + brewfileLine = mkBrewfileLineOption; }; config = let - configuredOptions = filterAttrs (_: v: v != null) (removeAttrs config [ "_module" "brewfileLine" ]); + configuredOptions = filterAttrs (_: v: v != null) + (removeAttrs config [ "_module" "brewfileLine" ]); in { brewfileLine = - if configuredOptions == {} - then null + if configuredOptions == {} then null else "cask_args " + mkBrewfileLineOptionsListString configuredOptions; }; }; @@ -266,10 +310,28 @@ in }; taps = mkOption { - type = with types; listOf str; + type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule tapOptions)); default = []; - example = [ "homebrew/cask-fonts" ]; - description = "Homebrew formula repositories to tap."; + example = literalExpression '' + # Adapted examples from https://github.com/Homebrew/homebrew-bundle#usage + [ + # 'brew tap' + "homebrew/cask" + # 'brew tap' with custom Git URL and arguments + { + name = "user/tap-repo"; + clone_target = "https://user@bitbucket.org/user/homebrew-tap-repo.git"; + force_auto_update = true; + } + ] + ''; + description = '' + Homebrew formula repositories to tap. + + Taps defined as strings, e.g., "user/repo", are a shorthand for: + + { name = "user/repo"; } + ''; }; brews = mkOption { @@ -338,9 +400,6 @@ in type = types.lines; default = ""; example = '' - # 'brew tap' with custom Git URL - tap "user/tap-repo", "https://user@bitbucket.org/user/homebrew-tap-repo.git" - # '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) From 56031db9c130f577f25a9bccbfc6d13e99c14b5f Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 22 Aug 2022 19:35:01 -0700 Subject: [PATCH 03/10] Enable defining options for `brews` using submodule --- modules/homebrew.nix | 105 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 355061b..faa884b 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -223,6 +223,75 @@ let else "cask_args " + mkBrewfileLineOptionsListString configuredOptions; }; }; + + brewOptions = { config, ... }: { + options = { + name = mkOption { + type = types.str; + description = "The name of the formula to install."; + }; + args = mkOption { + type = with types; nullOr (listOf str); + default = null; + description = '' + Arguments to pass to brew install. + ''; + }; + conflicts_with = mkOption { + type = with types; nullOr (listOf str); + default = null; + description = '' + List of formulae that should be unlinked and their services stopped (if they are + installed). + ''; + }; + restart_service = mkOption { + type = with types; nullOr (either bool (enum [ "changed" ])); + default = null; + description = '' + Whether to run 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. + + Homebrew's default is false. + ''; + }; + start_service = mkNullOrBoolOption { + description = '' + Whether to run brew services start for the formula and register it to + launch at login (or boot). + + Homebrew's default is false. + ''; + }; + link = mkNullOrBoolOption { + 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 it's currently unlinked and not keg-only, and to unlink the formula if it's + currently linked and keg-only. + ''; + }; + + brewfileLine = mkBrewfileLineOption; + }; + + config = + let + configuredOptions = filterAttrs (_: v: v != null) + (removeAttrs config [ "_module" "brewfileLine" "name" "restart_service" ]); + in + { + brewfileLine = ''brew "${config.name}"'' + + optionalString (configuredOptions != {}) + ", ${mkBrewfileLineOptionsListString configuredOptions}" + + optionalString (config.restart_service != null) ( + if isBool config.restart_service then + ", restart_service: ${boolToString config.restart_service}" + else ", restart_service: :${config.restart_service}" + ); + }; + }; in { @@ -335,10 +404,35 @@ in }; brews = mkOption { - type = with types; listOf str; + type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule brewOptions)); default = []; - example = [ "mas" ]; - description = "Homebrew brews to install."; + example = literalExpression '' + # Adapted examples from https://github.com/Homebrew/homebrew-bundle#usage + [ + # 'brew install' + "imagemagick" + # 'brew install --with-rmtp', 'brew services restart' on version changes + { + name = "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) + { + name = "mysql@5.6"; + restart_service = true; + link = true; + conflicts_with = [ "mysql" ]; + } + ] + ''; + description = '' + Homebrew brews to install. + + Brews defined as strings, e.g., "imagemagick", are a shorthand for: + + { name = "imagemagick"; } + ''; }; caskArgs = mkOption { @@ -400,11 +494,6 @@ in type = types.lines; default = ""; example = '' - # '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 From 02a38c6a8921fc78e868f499b020334b0b3963ad Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Mon, 22 Aug 2022 20:04:09 -0700 Subject: [PATCH 04/10] Enable defining options for `casks` using submodule --- modules/homebrew.nix | 61 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index faa884b..14acb06 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -18,7 +18,6 @@ let "\n" ); - brewfile = pkgs.writeText "Brewfile" ( mkBrewfileSectionString "Taps" "tap" cfg.taps + mkBrewfileSectionString "Arguments for all casks" "cask_args" @@ -292,6 +291,34 @@ let ); }; }; + + caskOptions = { config, ... }: { + options = { + name = mkOption { + type = types.str; + description = "The name of the cask to install."; + }; + args = mkOption { + type = types.nullOr (types.submodule caskArgsOptions); + default = null; + }; + greedy = mkNullOrBoolOption { + description = '' + Whether to always upgrade auto-updated or unversioned cask to latest version even if + already installed. + ''; + }; + + brewfileLine = mkBrewfileLineOption; + }; + + config = { + brewfileLine = ''cask "${config.name}"'' + + optionalString (config.args != null) + '', args: { ${removePrefix "cask_args " config.args.brewfileLine} }'' + + optionalString (config.greedy != null) ", greedy: ${boolToString config.greedy}"; + }; + }; in { @@ -446,10 +473,32 @@ in }; casks = mkOption { - type = with types; listOf str; + type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule caskOptions)); default = []; - example = [ "hammerspoon" "virtualbox" ]; - description = "Homebrew casks to install."; + example = literalExpression '' + # Adapted examples from https://github.com/Homebrew/homebrew-bundle#usage + [ + # 'brew install --cask' + "google-chrome" + # 'brew install --cask --appdir=~/my-apps/Applications' + { + name = "firefox"; + args = { appdir = "~/my-apps/Applications"; }; + } + # always upgrade auto-updated or unversioned cask to latest version even if already installed + { + name = "opera"; + greedy = true; + } + ] + ''; + description = '' + Homebrew casks to install. + + Casks defined as strings, e.g., "google-chrome", are a shorthand for: + + { name = "google-chrome"; } + ''; }; masApps = mkOption { @@ -494,12 +543,10 @@ in type = types.lines; default = ""; example = '' - # '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."; + description = "Extra lines to be added verbatim to bottom of the generated Brewfile."; }; }; From 46032bad426557be99cfbe2ad045d573814f69c9 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 23 Aug 2022 13:32:07 -0700 Subject: [PATCH 05/10] Cleanup/improve `homebrew` module's code, documentation, and option descriptions --- modules/homebrew.nix | 337 ++++++++++++++++++++++++++----------------- 1 file changed, 203 insertions(+), 134 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 14acb06..a0c97a9 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -6,36 +6,23 @@ with lib; let cfg = config.homebrew; - mkBrewfileSectionString = heading: type: entries: optionalString (entries != []) '' - # ${heading} - ${concatMapStringsSep "\n" (v: v.brewfileLine or ''${type} "${v}"'') entries} - - ''; - - mkMasBrewfileSectionString = entries: optionalString (entries != {}) ( - "# Mac App Store apps\n" + - concatStringsSep "\n" (mapAttrsToList (name: id: ''mas "${name}", id: ${toString id}'') entries) + - "\n" - ); - - brewfile = pkgs.writeText "Brewfile" ( - mkBrewfileSectionString "Taps" "tap" cfg.taps + - mkBrewfileSectionString "Arguments for all casks" "cask_args" - (optional (cfg.caskArgs.brewfileLine != null) cfg.caskArgs) + - mkBrewfileSectionString "Brews" "brew" cfg.brews + - mkBrewfileSectionString "Casks" "cask" cfg.casks + - mkMasBrewfileSectionString cfg.masApps + - mkBrewfileSectionString "Docker containers" "whalebrew" cfg.whalebrews + - optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig) - ); + brewfileFile = pkgs.writeText "Brewfile" cfg.brewfile; 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" + optional (!cfg.autoUpdate) "HOMEBREW_NO_AUTO_UPDATE=1" + ++ [ "brew bundle --file='${brewfileFile}' --no-lock" ] + ++ optional (cfg.cleanup == "uninstall" || cfg.cleanup == "zap") "--cleanup" + ++ optional (cfg.cleanup == "zap") "--zap" ); + # Brewfile creation helper functions ------------------------------------------------------------- + + mkBrewfileSectionString = heading: entries: optionalString (entries != [ ]) '' + # ${heading} + ${concatMapStringsSep "\n" (v: v.brewfileLine or v) entries} + + ''; + mkBrewfileLineValueString = v: if isInt v then toString v else if isFloat v then strings.floatToString v @@ -46,7 +33,10 @@ let else abort "The value: ${generators.toPretty v} is not a valid Brewfile value."; mkBrewfileLineOptionsListString = attrs: - concatStringsSep ", " (mapAttrsToList (n: v: "${n}: ${mkBrewfileLineValueString v}") attrs); + concatStringsSep ", " (mapAttrsToList (n: v: "${n}: ${v}") attrs); + + + # Submodule helper functions --------------------------------------------------------------------- mkNullOrBoolOption = args: mkOption (args // { type = types.nullOr types.bool; @@ -65,6 +55,18 @@ let readOnly = true; }; + mkProcessedSubmodConfig = attrs: mapAttrs (_: mkBrewfileLineValueString) + (filterAttrsRecursive (n: v: n != "_module" && n != "brewfileLine" && v != null) attrs); + + + # Submodules ------------------------------------------------------------------------------------- + # Option values and descriptions of Brewfile entries are sourced/derived from: + # * `brew` manpage: https://docs.brew.sh/Manpage + # * `brew bundle` source files (at https://github.com/Homebrew/homebrew-bundle/tree/9fffe077f1a5a722ed5bd26a87ed622e8cb64e0c): + # * lib/bundle/dsl.rb + # * lib/bundle/{brew,cask,tap}_installer.rb + # * spec/bundle/{brew,cask,tap}_installer_spec.rb + tapOptions = { config, ... }: { options = { name = mkOption { @@ -72,8 +74,8 @@ let example = "homebrew/cask-fonts"; description = '' When is unspecified, this is the name of a formula - repository to tap from GitHub using HTTPS. For example, "user/repo" will - tap https://github.com/user/homebrew-repo. + repository to tap from GitHub using HTTPS. For example, "user/repo" + will tap https://github.com/user/homebrew-repo. ''; }; clone_target = mkNullOrStrOption { @@ -94,12 +96,17 @@ let brewfileLine = mkBrewfileLineOption; }; - config = { - brewfileLine = ''tap "${config.name}"'' - + optionalString (config.clone_target != null) '', "${config.clone_target}"'' - + optionalString (config.force_auto_update != null) - ", force_auto_update: ${boolToString config.force_auto_update}"; - }; + config = + let + sCfg = mkProcessedSubmodConfig config; + in + { + brewfileLine = + "tap ${sCfg.name}" + + optionalString (sCfg ? clone_target) ", ${sCfg.clone_target}" + + optionalString (sCfg ? force_auto_update) + ", force_auto_update: ${sCfg.force_auto_update}"; + }; }; # Sourced from https://docs.brew.sh/Manpage#global-cask-options @@ -108,86 +115,100 @@ let options = { appdir = mkNullOrStrOption { description = '' - Target location for Applications - (default: /Applications) + Target location for Applications. + + Homebrew's default is /Applications. ''; }; colorpickerdir = mkNullOrStrOption { description = '' - Target location for Color Pickers - (default: ~/Library/ColorPickers) + Target location for Color Pickers. + + Homebrew's default is ~/Library/ColorPickers. ''; }; prefpanedir = mkNullOrStrOption { description = '' - Target location for Preference Panes - (default: ~/Library/PreferencePanes) + Target location for Preference Panes. + + Homebrew's default is ~/Library/PreferencePanes. ''; }; qlplugindir = mkNullOrStrOption { description = '' - Target location for QuickLook Plugins - (default: ~/Library/QuickLook) + Target location for QuickLook Plugins. + + Homebrew's default is ~/Library/QuickLook. ''; }; mdimporterdir = mkNullOrStrOption { description = '' - Target location for Spotlight Plugins - (default: ~/Library/Spotlight) + Target location for Spotlight Plugins. + + Homebrew's default is ~/Library/Spotlight. ''; }; dictionarydir = mkNullOrStrOption { description = '' - Target location for Dictionaries - (default: ~/Library/Dictionaries) + Target location for Dictionaries. + + Homebrew's default is ~/Library/Dictionaries. ''; }; fontdir = mkNullOrStrOption { description = '' - Target location for Fonts - (default: ~/Library/Fonts) + Target location for Fonts. + + Homebrew's default is ~/Library/Fonts. ''; }; servicedir = mkNullOrStrOption { description = '' - Target location for Services - (default: ~/Library/Services) + Target location for Services. + + Homebrew's default is ~/Library/Services. ''; }; input_methoddir = mkNullOrStrOption { description = '' - Target location for Input Methods - (default: ~/Library/Input Methods) + Target location for Input Methods. + + Homebrew's default is ~/Library/Input Methods. ''; }; internet_plugindir = mkNullOrStrOption { description = '' - Target location for Internet Plugins - (default: ~/Library/Internet Plug-Ins) + Target location for Internet Plugins. + + Homebrew's default is ~/Library/Internet Plug-Ins. ''; }; audio_unit_plugindir = mkNullOrStrOption { description = '' - Target location for Audio Unit Plugins - (default: ~/Library/Audio/Plug-Ins/Components) + Target location for Audio Unit Plugins. + + Homebrew's default is ~/Library/Audio/Plug-Ins/Components. ''; }; vst_plugindir = mkNullOrStrOption { description = '' - Target location for VST Plugins - (default: ~/Library/Audio/Plug-Ins/VST) + Target location for VST Plugins. + + Homebrew's default is ~/Library/Audio/Plug-Ins/VST. ''; }; vst3_plugindir = mkNullOrStrOption { description = '' - Target location for VST3 Plugins - (default: ~/Library/Audio/Plug-Ins/VST3) + Target location for VST3 Plugins. + + Homebrew's default is ~/Library/Audio/Plug-Ins/VST3. ''; }; screen_saverdir = mkNullOrStrOption { description = '' - Target location for Screen Savers - (default: ~/Library/Screen Savers) + Target location for Screen Savers. + + Homebrew's default is ~/Library/Screen Savers. ''; }; language = mkNullOrStrOption { @@ -213,13 +234,10 @@ let config = let - configuredOptions = filterAttrs (_: v: v != null) - (removeAttrs config [ "_module" "brewfileLine" ]); + sCfg = mkProcessedSubmodConfig config; in { - brewfileLine = - if configuredOptions == {} then null - else "cask_args " + mkBrewfileLineOptionsListString configuredOptions; + brewfileLine = if sCfg == { } then null else "cask_args ${mkBrewfileLineOptionsListString sCfg}"; }; }; @@ -233,7 +251,8 @@ let type = with types; nullOr (listOf str); default = null; description = '' - Arguments to pass to brew install. + Arguments flags to pass to brew install. Values should not include the + leading "--". ''; }; conflicts_with = mkOption { @@ -249,8 +268,8 @@ let default = null; description = '' Whether to run 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. + launch at login (or boot). If set to "changed", the service will only + be restarted on version changes. Homebrew's default is false. ''; @@ -267,7 +286,7 @@ let 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 it's currently unlinked and not keg-only, and to unlink the formula if it's + formula if it's currently unlinked and not keg-only, and to unlink the formula if it's currently linked and keg-only. ''; }; @@ -277,22 +296,25 @@ let config = let - configuredOptions = filterAttrs (_: v: v != null) - (removeAttrs config [ "_module" "brewfileLine" "name" "restart_service" ]); + sCfg = mkProcessedSubmodConfig config; + sCfgSubset = removeAttrs sCfg [ "name" "restart_service" ]; in { - brewfileLine = ''brew "${config.name}"'' - + optionalString (configuredOptions != {}) - ", ${mkBrewfileLineOptionsListString configuredOptions}" - + optionalString (config.restart_service != null) ( - if isBool config.restart_service then - ", restart_service: ${boolToString config.restart_service}" - else ", restart_service: :${config.restart_service}" + brewfileLine = + "brew ${sCfg.name}" + + optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}" + # We need to handle the `restart_service` option seperately since it can be either bool + # or the string value "changed". + + optionalString (sCfg ? restart_service) ( + ", restart_service: " + ( + if isBool config.restart_service then sCfg.restart_service + else ":${config.restart_service}" + ) ); }; }; - caskOptions = { config, ... }: { + caskOptions = { config, ... }: { options = { name = mkOption { type = types.str; @@ -304,38 +326,44 @@ let }; greedy = mkNullOrBoolOption { description = '' - Whether to always upgrade auto-updated or unversioned cask to latest version even if - already installed. + Whether to always upgrade auto-updated or unversioned cask to the latest version even if + it's already installed. ''; }; brewfileLine = mkBrewfileLineOption; }; - config = { - brewfileLine = ''cask "${config.name}"'' - + optionalString (config.args != null) - '', args: { ${removePrefix "cask_args " config.args.brewfileLine} }'' - + optionalString (config.greedy != null) ", greedy: ${boolToString config.greedy}"; - }; + config = + let + sCfg = mkProcessedSubmodConfig config; + sCfgSubset = removeAttrs sCfg [ "name" ]; + in + { + brewfileLine = + "cask ${sCfg.name}" + + optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}"; + }; }; in { + # Interface -------------------------------------------------------------------------------------- + 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 + Note that enabling this option does not install Homebrew. See the Homebrew + website for installation instructions ''; autoUpdate = mkOption { type = types.bool; default = false; description = '' - When enabled, Homebrew is allowed to auto-update during nix-darwin + Whether to enable Homebrew to auto-update during nix-darwin activation. The default is false so that repeated invocations of darwin-rebuild switch are idempotent. ''; @@ -344,8 +372,14 @@ in brewPrefix = mkOption { type = types.str; default = if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew/bin" else "/usr/local/bin"; + defaultText = literalExpression '' + if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew/bin" + else "/usr/local/bin" + ''; description = '' - Customize path prefix where executable of brew is searched for. + The path prefix where the 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. ''; }; @@ -381,11 +415,12 @@ in 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. + Whether to enable Homebrew to automatically use the Brewfile in the Nix store that this + module generates, when you manually invoke brew bundle. - 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 + Implementation note: when enabled, this option 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 . ''; }; @@ -394,26 +429,28 @@ in type = types.bool; default = false; description = '' - When enabled, lockfiles aren't generated when you manually invoke + Whether to disable lockfile generation 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 + Implementation note: when enabled, this option sets the + HOMEBREW_BUNDLE_NO_LOCK environment variable, by adding it to . ''; }; taps = mkOption { type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule tapOptions)); - default = []; + default = [ ]; example = literalExpression '' # Adapted examples from https://github.com/Homebrew/homebrew-bundle#usage [ - # 'brew tap' + # `brew tap` "homebrew/cask" - # 'brew tap' with custom Git URL and arguments + + # `brew tap` with custom Git URL and arguments { name = "user/tap-repo"; clone_target = "https://user@bitbucket.org/user/homebrew-tap-repo.git"; @@ -430,21 +467,35 @@ in ''; }; + caskArgs = mkOption { + type = types.submodule caskArgsOptions; + default = { }; + example = literalExpression '' + { + appdir = "~/Applications"; + require_sha = true; + } + ''; + description = "Arguments to apply to all ."; + }; + brews = mkOption { type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule brewOptions)); - default = []; + default = [ ]; example = literalExpression '' # Adapted examples from https://github.com/Homebrew/homebrew-bundle#usage [ - # 'brew install' + # `brew install` "imagemagick" - # 'brew install --with-rmtp', 'brew services restart' on version changes + + # `brew install --with-rmtp`, `brew services restart` on version changes { name = "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 install`, always `brew services restart`, `brew link`, `brew unlink mysql` (if it is installed) { name = "mysql@5.6"; restart_service = true; @@ -462,29 +513,21 @@ in ''; }; - caskArgs = mkOption { - type = types.submodule caskArgsOptions; - default = {}; - example = { - appdir = "~/Applications"; - require_sha = true; - }; - description = "Arguments to apply to all ."; - }; - casks = mkOption { type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule caskOptions)); - default = []; + default = [ ]; example = literalExpression '' # Adapted examples from https://github.com/Homebrew/homebrew-bundle#usage [ - # 'brew install --cask' + # `brew install --cask` "google-chrome" - # 'brew install --cask --appdir=~/my-apps/Applications' + + # `brew install --cask --appdir=~/my-apps/Applications` { name = "firefox"; args = { appdir = "~/my-apps/Applications"; }; } + # always upgrade auto-updated or unversioned cask to latest version even if already installed { name = "opera"; @@ -502,12 +545,14 @@ in }; masApps = mkOption { - type = with types; attrsOf ints.positive; - default = {}; - example = { - "1Password" = 1107421413; - Xcode = 497799835; - }; + type = types.attrsOf types.ints.positive; + default = { }; + example = literalExpression '' + { + "1Password for Safari" = 1569813296; + Xcode = 497799835; + } + ''; description = '' Applications to install from Mac App Store using mas. @@ -520,13 +565,14 @@ in 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. + For more information on mas see: + github.com/mas-cli/mas. ''; }; whalebrews = mkOption { type = with types; listOf str; - default = []; + default = [ ]; example = [ "whalebrew/wget" ]; description = '' Docker images to install using whalebrew. @@ -535,7 +581,7 @@ in . For more information on whalebrew see: - https://github.com/whalebrew/whalebrew. + github.com/whalebrew/whalebrew. ''; }; @@ -548,17 +594,40 @@ in ''; description = "Extra lines to be added verbatim to bottom of the generated Brewfile."; }; + + brewfile = mkOption { + type = types.str; + visible = false; + internal = true; + readOnly = true; + description = "String reprensentation of the generated Brewfile useful for debugging."; + }; }; + + # Implementation --------------------------------------------------------------------------------- + config = { homebrew.brews = - optional (cfg.masApps != {}) "mas" ++ - optional (cfg.whalebrews != []) "whalebrew"; + optional (cfg.masApps != { }) "mas" + ++ optional (cfg.whalebrews != [ ]) "whalebrew"; + + homebrew.brewfile = + "# Created by `nix-darwin`'s `homebrew` module\n\n" + + mkBrewfileSectionString "Taps" cfg.taps + + mkBrewfileSectionString "Arguments for all casks" + (optional (cfg.caskArgs.brewfileLine != null) cfg.caskArgs) + + mkBrewfileSectionString "Brews" cfg.brews + + mkBrewfileSectionString "Casks" cfg.casks + + mkBrewfileSectionString "Mac App Store apps" + (mapAttrsToList (n: id: ''mas "${n}", id: ${toString id}'') cfg.masApps) + + mkBrewfileSectionString "Docker containers" (map (v: ''whalebrew "${v}"'') cfg.whalebrews) + + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); environment.variables = mkIf cfg.enable ( - optionalAttrs cfg.global.brewfile { HOMEBREW_BUNDLE_FILE = "${brewfile}"; } // - optionalAttrs cfg.global.noLock { HOMEBREW_BUNDLE_NO_LOCK = "1"; } - ); + optionalAttrs cfg.global.brewfile { HOMEBREW_BUNDLE_FILE = "${brewfileFile}"; } + // optionalAttrs cfg.global.noLock { HOMEBREW_BUNDLE_NO_LOCK = "1"; } + ); system.activationScripts.homebrew.text = mkIf cfg.enable '' # Homebrew Bundle From 55e198cf5adf1d8e5c99fad1f7472669a8454d4f Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Tue, 23 Aug 2022 15:21:09 -0700 Subject: [PATCH 06/10] Add somes tests for `homebrew` module --- release.nix | 1 + tests/homebrew.nix | 101 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tests/homebrew.nix diff --git a/release.nix b/release.nix index a72ad98..4e5b093 100644 --- a/release.nix +++ b/release.nix @@ -101,6 +101,7 @@ let tests.autossh = makeTest ./tests/autossh.nix; tests.checks-nix-gc = makeTest ./tests/checks-nix-gc.nix; tests.environment-path = makeTest ./tests/environment-path.nix; + tests.homebrew = makeTest ./tests/homebrew.nix; tests.launchd-daemons = makeTest ./tests/launchd-daemons.nix; tests.launchd-setenv = makeTest ./tests/launchd-setenv.nix; tests.networking-hostname = makeTest ./tests/networking-hostname.nix; diff --git a/tests/homebrew.nix b/tests/homebrew.nix new file mode 100644 index 0000000..d7fdeab --- /dev/null +++ b/tests/homebrew.nix @@ -0,0 +1,101 @@ +{ config, lib, ... }: + +let + mkTest = filter: result: '' + if ! echo "$bf" | grep -F '${filter}' | grep -F '${result}' > /dev/null; then + echo Expected: + echo '${result}' + echo Actual: + echo "$bf" | grep -F '${filter}' + exit 1 + fi + ''; +in + +{ + homebrew.enable = true; + + # Examples taken from https://github.com/Homebrew/homebrew-bundle + homebrew.taps = [ + "homebrew/cask" + { + name = "user/tap-repo1"; + clone_target = "https://user@bitbucket.org/user/homebrew-tap-repo1.git"; + } + { + name = "user/tap-repo2"; + clone_target = "https://user@bitbucket.org/user/homebrew-tap-repo2.git"; + force_auto_update = true; + } + ]; + + homebrew.caskArgs = { + appdir = "~/Applications"; + require_sha = true; + }; + + homebrew.brews = [ + "imagemagick" + { + name = "denji/nginx/nginx-full"; + args = [ "with-rmtp" ]; + restart_service = "changed"; + } + { + name = "mysql@5.6"; + restart_service = true; + link = true; + conflicts_with = [ "mysql" ]; + } + ]; + + homebrew.casks = [ + "google-chrome" + { + name = "firefox"; + args = { appdir = "~/my-apps/Applications"; }; + } + { + name = "opera"; + greedy = true; + } + ]; + + homebrew.masApps = { + "1Password for Safari" = 1569813296; + Xcode = 497799835; + }; + + homebrew.whalebrews = [ + "whalebrew/wget" + ]; + + test = '' + bf=${lib.escapeShellArg config.homebrew.brewfile} + + echo "checking tap entries in Brewfile" >&2 + ${mkTest "homebrew/cask" ''tap "homebrew/cask"''} + ${mkTest "user/tap-repo1" ''tap "user/tap-repo1", "https://user@bitbucket.org/user/homebrew-tap-repo1.git"''} + ${mkTest "user/tap-repo2" ''tap "user/tap-repo2", "https://user@bitbucket.org/user/homebrew-tap-repo2.git", force_auto_update: true''} + + echo "checking cask_args entry in Brewfile" >&2 + ${mkTest "cask_args" ''cask_args appdir: "~/Applications", require_sha: true''} + + 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 "mysql@5.6" ''brew "mysql@5.6", conflicts_with: ["mysql"], link: true, restart_service: true''} + + 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''} + + echo "checking mas entries in Brewfile" >&2 + ${mkTest "1Password for Safari" ''mas "1Password for Safari", id: 1569813296''} + ${mkTest "Xcode" ''mas "Xcode", id: 497799835''} + + echo "checking whalebrew entries in Brewfile" >&2 + ${mkTest "whalebrew/wget" ''whalebrew "whalebrew/wget"''} + ''; +} From b547a7acb09d03bb6a41c6235c1d7f380981073d Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Wed, 24 Aug 2022 13:07:19 -0700 Subject: [PATCH 07/10] Create submodule for activation related `homebrew` options * Adds `homebrew.onActivation` submodule. * Moves `homebrew.autoUpdate` to `homebrew.onActivation.autoUpdate`. * Moves `homebrew.cleanup` to `homebrew.onActivation.clean`. * Adds new option `homebrew.onActivation.upgrade`. --- modules/homebrew.nix | 133 ++++++++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index a0c97a9..2a130f0 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -1,5 +1,5 @@ # Created by: https://github.com/malob -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: with lib; @@ -8,13 +8,6 @@ let brewfileFile = pkgs.writeText "Brewfile" cfg.brewfile; - brew-bundle-command = concatStringsSep " " ( - optional (!cfg.autoUpdate) "HOMEBREW_NO_AUTO_UPDATE=1" - ++ [ "brew bundle --file='${brewfileFile}' --no-lock" ] - ++ optional (cfg.cleanup == "uninstall" || cfg.cleanup == "zap") "--cleanup" - ++ optional (cfg.cleanup == "zap") "--zap" - ); - # Brewfile creation helper functions ------------------------------------------------------------- mkBrewfileSectionString = heading: entries: optionalString (entries != [ ]) '' @@ -67,6 +60,75 @@ let # * lib/bundle/{brew,cask,tap}_installer.rb # * spec/bundle/{brew,cask,tap}_installer_spec.rb + onActivationOptions = { config, ... }: { + options = { + cleanup = mkOption { + type = types.enum [ "none" "uninstall" "zap" ]; + default = "none"; + example = "uninstall"; + description = '' + This option manages what happens to formulae installed by Homebrew, that aren't present in + the Brewfile generated by this module, during nix-darwin activation. + + When set to "none" (the default), formulae 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 formulae not listed in generate Brewfile, i.e., + brew uninstall is run for those formulae. + + When set to "zap", nix-darwin invokes + brew bundle [install] with the --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, + brew uninstall --zap is run for all those formulae. + + If you plan on exclusively using nix-darwin to manage formulae + installed by Homebrew, you probably want to set this option to + "uninstall" or "zap". + ''; + }; + autoUpdate = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Homebrew to auto-update itself and all formulae during + nix-darwin activation. The default is false so that + repeated invocations of darwin-rebuild switch are idempotent. + + Note that Homebrew auto-updates when it's been more then 5 minutes since it last updated. + ''; + }; + upgrade = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Homebrew to upgrade outdated formulae and Mac App Store apps during + nix-darwin activation. The default is false so + that repeated invocations of darwin-rebuild switch are idempotent. + ''; + }; + + brewBundleCmd = mkOption { + type = types.str; + visible = false; + internal = true; + readOnly = true; + }; + }; + + config = { + brewBundleCmd = concatStringsSep " " ( + optional (!config.autoUpdate) "HOMEBREW_NO_AUTO_UPDATE=1" + ++ [ "brew bundle --file='${brewfileFile}' --no-lock" ] + ++ optional (!config.upgrade) "--no-upgrade" + ++ optional (config.cleanup == "uninstall") "--cleanup" + ++ optional (config.cleanup == "zap") "--cleanup --zap" + ); + }; + }; + tapOptions = { config, ... }: { options = { name = mkOption { @@ -350,25 +412,20 @@ in { # Interface -------------------------------------------------------------------------------------- + imports = [ + (mkRenamedOptionModule [ "homebrew" "autoUpdate" ] [ "homebrew" "onActivation" "autoUpdate" ]) + (mkRenamedOptionModule [ "homebrew" "cleanup" ] [ "homebrew" "onActivation" "cleanup" ]) + ]; + options.homebrew = { enable = mkEnableOption '' - configuring your Brewfile, and installing/updating the formulas therein via + configuring your Brewfile, and installing/updating the formulae 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 ''; - autoUpdate = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable Homebrew to auto-update during nix-darwin - activation. The default is false so that repeated invocations of - darwin-rebuild switch are idempotent. - ''; - }; - brewPrefix = mkOption { type = types.str; default = if pkgs.stdenv.hostPlatform.isAarch64 then "/opt/homebrew/bin" else "/usr/local/bin"; @@ -383,31 +440,12 @@ in ''; }; - cleanup = mkOption { - type = types.enum [ "none" "uninstall" "zap" ]; - default = "none"; - example = "uninstall"; + onActivation = mkOption { + type = types.submodule onActivationOptions; + default = { }; 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". + Options for configuring the behavior of the brew bundle command that + nix-darwin runs during system activation. ''; }; @@ -562,7 +600,7 @@ in 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" + is set to "uninstall" or "zap" (this is currently a limitation of Homebrew Bundle). For more information on mas see: @@ -608,6 +646,11 @@ in # Implementation --------------------------------------------------------------------------------- config = { + + 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' activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.") + ]; + homebrew.brews = optional (cfg.masApps != { }) "mas" ++ optional (cfg.whalebrews != [ ]) "whalebrew"; @@ -633,7 +676,7 @@ in # Homebrew Bundle echo >&2 "Homebrew bundle..." if [ -f "${cfg.brewPrefix}/brew" ]; then - PATH="${cfg.brewPrefix}":$PATH ${brew-bundle-command} + PATH="${cfg.brewPrefix}":$PATH ${cfg.onActivation.brewBundleCmd} else echo -e "\e[1;31merror: Homebrew is not installed, skipping...\e[0m" >&2 fi From 7710d1d7d64e67a90571828d10957d200f793127 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Wed, 24 Aug 2022 17:26:50 -0700 Subject: [PATCH 08/10] Add `global` into a submodule * Add `homebrew.global.autoUpdate` option. * Remove `homebrew.global.noLock` option, and replace it with `hombrew.global.lockfiles`. --- modules/homebrew.nix | 158 +++++++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 49 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index 2a130f0..d462aae 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -29,7 +29,10 @@ let concatStringsSep ", " (mapAttrsToList (n: v: "${n}: ${v}") attrs); - # Submodule helper functions --------------------------------------------------------------------- + # Option and submodule helper functions ---------------------------------------------------------- + + mkDocOptionLink = optionName: + ''''; mkNullOrBoolOption = args: mkOption (args // { type = types.nullOr types.bool; @@ -41,12 +44,11 @@ let default = null; }); - mkBrewfileLineOption = mkOption { - type = types.nullOr types.str; + mkInternalOption = args: mkOption (args // { visible = false; internal = true; readOnly = true; - }; + }); mkProcessedSubmodConfig = attrs: mapAttrs (_: mkBrewfileLineValueString) (filterAttrsRecursive (n: v: n != "_module" && n != "brewfileLine" && v != null) attrs); @@ -110,12 +112,7 @@ let ''; }; - brewBundleCmd = mkOption { - type = types.str; - visible = false; - internal = true; - readOnly = true; - }; + brewBundleCmd = mkInternalOption { type = types.str; }; }; config = { @@ -129,6 +126,90 @@ let }; }; + globalOptions = { config, ... }: { + options = { + brewfile = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Homebrew to automatically use the Brewfile that this module generates in + the Nix store, when you manually invoke brew bundle. + + Enabling this option will change the default value of + ${mkDocOptionLink "homebrew.global.lockfiles"} to false since, with + this option enabled, 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 + ${mkDocOptionLink "environment.variables"}. + ''; + }; + autoUpdate = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable Homebrew to auto-update itself and all formulae when you manually invoke + commands like brew install, brew upgrade, + brew tap, and 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. + + You may want to consider disabling this option if you have + ${mkDocOptionLink "homebrew.onActivation.upgrade"} enabled, and + ${mkDocOptionLink "homebrew.onActivation.autoUpdate"} disabled, if you want to ensure that + your installed formulae will only be upgraded during nix-darwin system + activation, after you've explicitly run brew update. + + Implementation note: when disabled, this option sets the + HOMEBREW_NO_AUTO_UPDATE environment variable, by adding it to + ${mkDocOptionLink "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 + brew bundle [install]. + + This option will default to false if + ${mkDocOptionLink "homebrew.global.brewfile"} is enabled since, with that option enabled, + 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 + ${mkDocOptionLink "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 = mkOption { visible = false; default = null; }; + + homebrewEnvironmentVariables = mkInternalOption { type = types.attrs; }; + }; + + config = { + homebrewEnvironmentVariables = { + HOMEBREW_BUNDLE_FILE = mkIf config.brewfile "${brewfileFile}"; + HOMEBREW_NO_AUTO_UPDATE = mkIf (!config.autoUpdate) "1"; + HOMEBREW_BUNDLE_NO_LOCK = mkIf (!config.lockfiles) "1"; + }; + }; + }; + tapOptions = { config, ... }: { options = { name = mkOption { @@ -155,7 +236,7 @@ let ''; }; - brewfileLine = mkBrewfileLineOption; + brewfileLine = mkInternalOption { type = types.nullOr types.str; }; }; config = @@ -291,7 +372,7 @@ let description = "Whether to disable linking of helper executables."; }; - brewfileLine = mkBrewfileLineOption; + brewfileLine = mkInternalOption { type = types.nullOr types.str; }; }; config = @@ -299,7 +380,9 @@ let sCfg = mkProcessedSubmodConfig config; in { - brewfileLine = if sCfg == { } then null else "cask_args ${mkBrewfileLineOptionsListString sCfg}"; + brewfileLine = + if sCfg == { } then null + else "cask_args ${mkBrewfileLineOptionsListString sCfg}"; }; }; @@ -353,7 +436,7 @@ let ''; }; - brewfileLine = mkBrewfileLineOption; + brewfileLine = mkInternalOption { type = types.nullOr types.str; }; }; config = @@ -393,7 +476,7 @@ let ''; }; - brewfileLine = mkBrewfileLineOption; + brewfileLine = mkInternalOption { type = types.nullOr types.str; }; }; config = @@ -449,33 +532,11 @@ in ''; }; - global.brewfile = mkOption { - type = types.bool; - default = false; + global = mkOption { + type = types.submodule globalOptions; + default = { }; description = '' - Whether to enable Homebrew to automatically use the Brewfile in the Nix store that this - module generates, when you manually invoke brew bundle. - - Implementation note: when enabled, this option 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 = '' - Whether to disable lockfile generation 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). - - Implementation note: when enabled, this option sets the - HOMEBREW_BUNDLE_NO_LOCK environment variable, by adding it to - . + Options for configuring the behavior of Homebrew commands when you manually invoke them. ''; }; @@ -633,11 +694,8 @@ in description = "Extra lines to be added verbatim to bottom of the generated Brewfile."; }; - brewfile = mkOption { + brewfile = mkInternalOption { type = types.str; - visible = false; - internal = true; - readOnly = true; description = "String reprensentation of the generated Brewfile useful for debugging."; }; }; @@ -647,6 +705,11 @@ 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' activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.") ]; @@ -667,10 +730,7 @@ in + mkBrewfileSectionString "Docker containers" (map (v: ''whalebrew "${v}"'') cfg.whalebrews) + optionalString (cfg.extraConfig != "") ("# Extra config\n" + cfg.extraConfig); - environment.variables = mkIf cfg.enable ( - optionalAttrs cfg.global.brewfile { HOMEBREW_BUNDLE_FILE = "${brewfileFile}"; } - // optionalAttrs cfg.global.noLock { HOMEBREW_BUNDLE_NO_LOCK = "1"; } - ); + environment.variables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables; system.activationScripts.homebrew.text = mkIf cfg.enable '' # Homebrew Bundle From 2ddebb3189ccd54c7741f6feaaa9d131685a62c5 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Wed, 24 Aug 2022 17:35:42 -0700 Subject: [PATCH 09/10] Improve documentation of `homebrew` module --- modules/homebrew.nix | 90 +++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/modules/homebrew.nix b/modules/homebrew.nix index d462aae..200e0d7 100644 --- a/modules/homebrew.nix +++ b/modules/homebrew.nix @@ -70,14 +70,15 @@ let example = "uninstall"; description = '' This option manages what happens to formulae installed by Homebrew, that aren't present in - the Brewfile generated by this module, during nix-darwin activation. + the Brewfile generated by this module, during nix-darwin system + activation. When set to "none" (the default), formulae 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 formulae not listed in generate Brewfile, i.e., + uninstalls all formulae not listed in generated Brewfile, i.e., brew uninstall is run for those formulae. When set to "zap", nix-darwin invokes @@ -96,10 +97,14 @@ let default = false; description = '' Whether to enable Homebrew to auto-update itself and all formulae during - nix-darwin activation. The default is false so that - repeated invocations of darwin-rebuild switch are idempotent. + nix-darwin system activation. The default is false + so that repeated invocations of darwin-rebuild switch are idempotent. Note that Homebrew auto-updates when it's been more then 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 + behavior see ${mkDocOptionLink "homebrew.global.autoUpdate"}. ''; }; upgrade = mkOption { @@ -107,8 +112,8 @@ let default = false; description = '' Whether to enable Homebrew to upgrade outdated formulae and Mac App Store apps during - nix-darwin activation. The default is false so - that repeated invocations of darwin-rebuild switch are idempotent. + nix-darwin system activation. The default is false + so that repeated invocations of darwin-rebuild switch are idempotent. ''; }; @@ -330,7 +335,8 @@ let description = '' Target location for Audio Unit Plugins. - Homebrew's default is ~/Library/Audio/Plug-Ins/Components. + Homebrew's default is + ~/Library/Audio/Plug-Ins/Components. ''; }; vst_plugindir = mkNullOrStrOption { @@ -363,7 +369,11 @@ let example = "zh-TW"; }; require_sha = mkNullOrBoolOption { - description = "Whether to require cask(s) to have a checksum."; + description = '' + Whether to require cask(s) to have a checksum. + + Homebrew's default is false. + ''; }; no_quarantine = mkNullOrBoolOption { description = "Whether to disable quarantining of downloads."; @@ -448,8 +458,8 @@ let brewfileLine = "brew ${sCfg.name}" + optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}" - # We need to handle the `restart_service` option seperately since it can be either bool - # or the string value "changed". + # 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 @@ -468,11 +478,16 @@ let args = mkOption { type = types.nullOr (types.submodule caskArgsOptions); default = null; + visible = "shallow"; # so that options from `homebrew.caskArgs` aren't repeated. + description = '' + Arguments passed to brew install --cask when installing this cask. See + ${mkDocOptionLink "homebrew.caskArgs"} for the available options. + ''; }; greedy = mkNullOrBoolOption { description = '' - Whether to always upgrade auto-updated or unversioned cask to the latest version even if - it's already installed. + Whether to always upgrade this cask regardless of whether it's unversioned or it updates + itself. ''; }; @@ -502,11 +517,29 @@ in options.homebrew = { enable = mkEnableOption '' - configuring your Brewfile, and installing/updating the formulae therein via - the brew bundle command, using nix-darwin. + 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. - Note that enabling this option does not install Homebrew. See the Homebrew - website for installation instructions + Note that enabling this option does not install Homebrew, see the Homebrew + website for installation instructions. + + Use the ${mkDocOptionLink "homebrew.brews"}, ${mkDocOptionLink "homebrew.casks"}, + ${mkDocOptionLink "homebrew.masApps"}, and ${mkDocOptionLink "homebrew.whalebrews"} options + to list the Homebrew formulae, casks, Mac App Store apps, and Docker containers you'd like to + install. Use the ${mkDocOptionLink "homebrew.taps"} option, to make additional formula + repositories available to Homebrew. This module uses those options (along with the + ${mkDocOptionLink "homebrew.caskArgs"} options) to generate a Brewfile that + nix-darwin passes to the brew bundle command during + system activation. + + The default configuration of this module prevents Homebrew Bundle from auto-updating Homebrew + and all formulae, as well as upgrading anything that's already installed, so that repeated + invocations of darwin-rebuild switch (without any change to the + configuration) are idempotent. You can modify this behavior using the options under + ${mkDocOptionLink "homebrew.onActivation"}. + + This module also provides a few options for modifying how Homebrew commands behave when + you manually invoke them, under ${mkDocOptionLink "homebrew.global"} ''; brewPrefix = mkOption { @@ -558,7 +591,7 @@ in ] ''; description = '' - Homebrew formula repositories to tap. + List of Homebrew formula repositories to tap. Taps defined as strings, e.g., "user/repo", are a shorthand for: @@ -575,7 +608,10 @@ in require_sha = true; } ''; - description = "Arguments to apply to all ."; + description = '' + Arguments passed to brew install --cask for all casks listed in + ${mkDocOptionLink "homebrew.casks"}. + ''; }; brews = mkOption { @@ -604,9 +640,9 @@ in ] ''; description = '' - Homebrew brews to install. + List of Homebrew formulae to install. - Brews defined as strings, e.g., "imagemagick", are a shorthand for: + Formulae defined as strings, e.g., "imagemagick", are a shorthand for: { name = "imagemagick"; } ''; @@ -635,7 +671,7 @@ in ] ''; description = '' - Homebrew casks to install. + List of Homebrew casks to install. Casks defined as strings, e.g., "google-chrome", are a shorthand for: @@ -656,12 +692,12 @@ in Applications to install from Mac App Store using mas. When this option is used, "mas" is automatically added to - . + ${mkDocOptionLink "homebrew.brews"}. 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" + ${mkDocOptionLink "homebrew.onActivation.cleanup"} is set to "uninstall" or "zap" (this is currently a limitation of Homebrew Bundle). For more information on mas see: @@ -674,10 +710,10 @@ in default = [ ]; example = [ "whalebrew/wget" ]; description = '' - Docker images to install using whalebrew. + List of Docker images to install using whalebrew. When this option is used, "whalebrew" is automatically added to - . + ${mkDocOptionLink "homebrew.brews"}. For more information on whalebrew see: github.com/whalebrew/whalebrew. @@ -691,7 +727,7 @@ in # '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 bottom of the generated Brewfile."; + description = "Extra lines to be added verbatim to the bottom of the generated Brewfile."; }; brewfile = mkInternalOption { @@ -711,7 +747,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' activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.") + (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'.") ]; homebrew.brews = From 9b3104f90d67c233f8f9c212ef31750d0ccd6bf9 Mon Sep 17 00:00:00 2001 From: Malo Bourgon Date: Wed, 24 Aug 2022 17:53:45 -0700 Subject: [PATCH 10/10] Update changelog --- CHANGELOG | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b4f5cb1..b307cca 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,24 @@ +2022-08-24 +- Major changes to `homebrew` module + `homebrew.cleanup` was renamed to `homebrew.onActivation.cleanup`. + + `homebrew.autoUpdate` was renamed to `homebrew.onActivation.autoUpdate`. + + `homebrew.onActivation.upgrade` was added, and the default behavior of the + module was changed to not upgrade installed formulae and apps. + + `homebrew.global.autoUpdate` was added. + + `homebrew.global.noLock` was replaced with `hombrew.global.lockfiles`. + + `homebrew.caskArgs` submodule was added for configuring arguments to be used + for all casks. + + `homebrew.{taps,brews,casks}` were reimplemented as lists of submodules, + instead of lists of strings, with well documented options for all available + arguments for these Brewfile entry types, while preserving backwards + compatibility with the previous implementation. + 2022-08-14 - nix module updated to bring it back in sync with it's NixOS counterpart It should now be much more fiesable to share code for this module between