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