Skip to content

Instantly share code, notes, and snippets.

@therealpxc
Last active April 24, 2024 18:39
Show Gist options
  • Save therealpxc/0f69398a33c33b97fa93f811b8175d16 to your computer and use it in GitHub Desktop.
Save therealpxc/0f69398a33c33b97fa93f811b8175d16 to your computer and use it in GitHub Desktop.
a Home Manager module for Homebrew, copied from the Nix-Darwin module for Homebrew
# Created by: https://github.com/malob
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.homebrew;
brewfileFile = pkgs.writeText "Brewfile" cfg.brewfile;
# 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
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}: ${v}") attrs);
# Option and submodule helper functions ----------------------------------------------------------
mkNullOrBoolOption = args: mkOption (args // {
type = types.nullOr types.bool;
default = null;
});
mkNullOrStrOption = args: mkOption (args // {
type = types.nullOr types.str;
default = null;
});
mkInternalOption = args: mkOption (args // {
visible = false;
internal = true;
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
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 {command}`nix-darwin` system
activation.
When set to `"none"` (the default), formulae not present in the generated
Brewfile are left installed.
When set to `"uninstall"`, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--cleanup` flag. This
uninstalls all formulae not listed in generated Brewfile, i.e.,
{command}`brew uninstall` is run for those formulae.
When set to `"zap"`, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--cleanup --zap`
flags. This uninstalls all formulae not listed in the generated Brewfile, and if the
formula is a cask, removes all files associated with that cask. In other words,
{command}`brew uninstall --zap` is run for all those formulae.
If you plan on exclusively using {command}`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
{command}`nix-darwin` system activation. The default is `false`
so that repeated invocations of {command}`darwin-rebuild switch` are idempotent.
Note that Homebrew auto-updates when it's been more then 5 minutes since it last updated.
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 [](#opt-homebrew.global.autoUpdate).
Implementation note: when disabled, this option sets the `HOMEBREW_NO_AUTO_UPDATE`
environment variable when {command}`nix-darwin` invokes {command}`brew bundle [install]`
during system activation.
'';
};
upgrade = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Homebrew to upgrade outdated formulae and Mac App Store apps during
{command}`nix-darwin` system activation. The default is `false`
so that repeated invocations of {command}`darwin-rebuild switch` are idempotent.
Implementation note: when disabled, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--no-upgrade` flag during system
activation.
'';
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--verbose" ];
description = ''
Extra flags to pass to {command}`brew bundle [install]` during {command}`nix-darwin`
system activation.
'';
};
brewBundleCmd = mkInternalOption { type = types.str; };
};
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"
++ config.extraFlags
);
};
};
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 {command}`brew bundle`.
Enabling this option will change the default value of
[](#opt-homebrew.global.lockfiles) to `false` since, with
this option enabled, {command}`brew bundle [install]` will default to using the
Brewfile that this module generates in the Nix store, unless you explicitly point it at
another Brewfile using the `--file` flag. As a result, it will try to
write the lockfile in the Nix store, and complain that it can't (though the command will
run successfully regardless).
Implementation note: when enabled, this option sets the
`HOMEBREW_BUNDLE_FILE` environment variable to the path of the Brewfile
that this module generates in the Nix store, by adding it to
[](#opt-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 {command}`brew install`, {command}`brew upgrade`,
{command}`brew tap`, and {command}`brew bundle [install]`.
Note that Homebrew auto-updates when you manually invoke commands like the ones mentioned
above if it's been more then 5 minutes since it last updated.
You may want to consider disabling this option if you have
[](#opt-homebrew.onActivation.upgrade) enabled, and
[](#opt-homebrew.onActivation.autoUpdate) disabled, if you want to ensure that
your installed formulae will only be upgraded during {command}`nix-darwin` system
activation, after you've explicitly run {command}`brew update`.
Implementation note: when disabled, this option sets the
`HOMEBREW_NO_AUTO_UPDATE` environment variable, by adding it to
[](#opt-environment.variables).
'';
};
lockfiles = mkOption {
type = types.bool;
default = !config.brewfile;
defaultText = literalExpression "!config.homebrew.global.brewfile";
description = ''
Whether to enable Homebrew to generate lockfiles when you manually invoke
{command}`brew bundle [install]`.
This option will default to `false` if
[](#opt-homebrew.global.brewfile) is enabled since, with that option enabled,
{command}`brew bundle [install]` will default to using the Brewfile that this
module generates in the Nix store, unless you explicitly point it at another Brewfile
using the `--file` flag. As a result, it will try to write the
lockfile in the Nix store, and complain that it can't (though the command will run
successfully regardless).
Implementation note: when disabled, this option sets the
`HOMEBREW_BUNDLE_NO_LOCK` environment variable, by adding it to
[](#opt-environment.variables).
'';
};
# The `noLock` option was replaced by `lockfiles`. Due to `homebrew.global` being a submodule,
# we can't use `mkRemovedOptionModule`, so we leave this option definition here, and trigger
# and error message with an assertion below if it's set by the user.
noLock = 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 {
type = types.str;
example = "homebrew/cask-fonts";
description = ''
When {option}`clone_target` 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 {command}`git` handles. When {option}`clone_target` 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 = mkInternalOption { type = types.nullOr types.str; };
};
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
# and valid values for `HOMEBREW_CASK_OPTS`.
caskArgsOptions = { config, ... }: {
options = {
appdir = mkNullOrStrOption {
description = ''
Target location for Applications.
Homebrew's default is {file}`/Applications`.
'';
};
colorpickerdir = mkNullOrStrOption {
description = ''
Target location for Color Pickers.
Homebrew's default is {file}`~/Library/ColorPickers`.
'';
};
prefpanedir = mkNullOrStrOption {
description = ''
Target location for Preference Panes.
Homebrew's default is {file}`~/Library/PreferencePanes`.
'';
};
qlplugindir = mkNullOrStrOption {
description = ''
Target location for QuickLook Plugins.
Homebrew's default is {file}`~/Library/QuickLook`.
'';
};
mdimporterdir = mkNullOrStrOption {
description = ''
Target location for Spotlight Plugins.
Homebrew's default is {file}`~/Library/Spotlight`.
'';
};
dictionarydir = mkNullOrStrOption {
description = ''
Target location for Dictionaries.
Homebrew's default is {file}`~/Library/Dictionaries`.
'';
};
fontdir = mkNullOrStrOption {
description = ''
Target location for Fonts.
Homebrew's default is {file}`~/Library/Fonts`.
'';
};
servicedir = mkNullOrStrOption {
description = ''
Target location for Services.
Homebrew's default is {file}`~/Library/Services`.
'';
};
input_methoddir = mkNullOrStrOption {
description = ''
Target location for Input Methods.
Homebrew's default is {file}`~/Library/Input Methods`.
'';
};
internet_plugindir = mkNullOrStrOption {
description = ''
Target location for Internet Plugins.
Homebrew's default is {file}`~/Library/Internet Plug-Ins`.
'';
};
audio_unit_plugindir = mkNullOrStrOption {
description = ''
Target location for Audio Unit Plugins.
Homebrew's default is
{file}`~/Library/Audio/Plug-Ins/Components`.
'';
};
vst_plugindir = mkNullOrStrOption {
description = ''
Target location for VST Plugins.
Homebrew's default is {file}`~/Library/Audio/Plug-Ins/VST`.
'';
};
vst3_plugindir = mkNullOrStrOption {
description = ''
Target location for VST3 Plugins.
Homebrew's default is {file}`~/Library/Audio/Plug-Ins/VST3`.
'';
};
screen_saverdir = mkNullOrStrOption {
description = ''
Target location for Screen Savers.
Homebrew's default is {file}`~/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.
Homebrew's default is `false`.
'';
};
no_quarantine = mkNullOrBoolOption {
description = "Whether to disable quarantining of downloads.";
};
no_binaries = mkNullOrBoolOption {
description = "Whether to disable linking of helper executables.";
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
in
{
brewfileLine =
if sCfg == { } then null
else "cask_args ${mkBrewfileLineOptionsListString sCfg}";
};
};
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 flags to pass to {command}`brew install`. Values should not include the
leading `"--"`.
'';
};
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 {command}`brew services restart` for the formula and register it to
launch at login (or boot). If set to `"changed"`, the service will only
be restarted on version changes.
Homebrew's default is `false`.
'';
};
start_service = mkNullOrBoolOption {
description = ''
Whether to run {command}`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 if it's currently unlinked and not keg-only, and to unlink the formula if it's
currently linked and keg-only.
'';
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
sCfgSubset = removeAttrs sCfg [ "name" "restart_service" ];
in
{
brewfileLine =
"brew ${sCfg.name}"
+ optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}"
# We need to handle the `restart_service` option seperately since it can be either a bool
# or `:changed` in the Brewfile.
+ optionalString (sCfg ? restart_service) (
", restart_service: " + (
if isBool config.restart_service then sCfg.restart_service
else ":${config.restart_service}"
)
);
};
};
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;
visible = "shallow"; # so that options from `homebrew.caskArgs` aren't repeated.
description = ''
Arguments passed to {command}`brew install --cask` when installing this cask. See
[](#opt-homebrew.caskArgs) for the available options.
'';
};
greedy = mkNullOrBoolOption {
description = ''
Whether to always upgrade this cask regardless of whether it's unversioned or it updates
itself.
'';
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
sCfgSubset = removeAttrs sCfg [ "name" ];
in
{
brewfileLine =
"cask ${sCfg.name}"
+ optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}";
};
};
in
{
# Interface --------------------------------------------------------------------------------------
imports = [
(mkRenamedOptionModule [ "homebrew" "autoUpdate" ] [ "homebrew" "onActivation" "autoUpdate" ])
(mkRenamedOptionModule [ "homebrew" "cleanup" ] [ "homebrew" "onActivation" "cleanup" ])
];
options.homebrew = {
enable = mkEnableOption ''
{command}`nix-darwin` to manage installing/updating/upgrading Homebrew taps, formulae,
and casks, as well as Mac App Store apps and Docker containers, using Homebrew Bundle.
Note that enabling this option does not install Homebrew, see the Homebrew
[website](https://brew.sh) for installation instructions.
Use the [](#opt-homebrew.brews), [](#opt-homebrew.casks),
[](#opt-homebrew.masApps), and [](#opt-homebrew.whalebrews) options
to list the Homebrew formulae, casks, Mac App Store apps, and Docker containers you'd like to
install. Use the [](#opt-homebrew.taps) option, to make additional formula
repositories available to Homebrew. This module uses those options (along with the
[](#opt-homebrew.caskArgs) options) to generate a Brewfile that
{command}`nix-darwin` passes to the {command}`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 {command}`darwin-rebuild switch` (without any change to the
configuration) are idempotent. You can modify this behavior using the options under
[](#opt-homebrew.onActivation).
This module also provides a few options for modifying how Homebrew commands behave when
you manually invoke them, under [](#opt-homebrew.global)'';
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 = ''
The path prefix where the {command}`brew` executable is located. This will be set to
the correct value based on your system's platform, and should only need to be changed if you
manually installed Homebrew in a non-standard location.
'';
};
onActivation = mkOption {
type = types.submodule onActivationOptions;
default = { };
description = ''
Options for configuring the behavior of the {command}`brew bundle` command that
{command}`nix-darwin` runs during system activation.
'';
};
global = mkOption {
type = types.submodule globalOptions;
default = { };
description = ''
Options for configuring the behavior of Homebrew commands when you manually invoke them.
'';
};
taps = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule tapOptions));
default = [ ];
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 = ''
List of Homebrew formula repositories to tap.
Taps defined as strings, e.g., `"user/repo"`, are a shorthand for:
`{ name = "user/repo"; }`
'';
};
caskArgs = mkOption {
type = types.submodule caskArgsOptions;
default = { };
example = literalExpression ''
{
appdir = "~/Applications";
require_sha = true;
}
'';
description = ''
Arguments passed to {command}`brew install --cask` for all casks listed in
[](#opt-homebrew.casks).
'';
};
brews = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule brewOptions));
default = [ ];
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 = ''
List of Homebrew formulae to install.
Formulae defined as strings, e.g., `"imagemagick"`, are a shorthand for:
`{ name = "imagemagick"; }`
'';
};
casks = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule caskOptions));
default = [ ];
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 = ''
List of Homebrew casks to install.
Casks defined as strings, e.g., `"google-chrome"`, are a shorthand for:
`{ name = "google-chrome"; }`
'';
};
masApps = mkOption {
type = types.attrsOf types.ints.positive;
default = { };
example = literalExpression ''
{
"1Password for Safari" = 1569813296;
Xcode = 497799835;
}
'';
description = ''
Applications to install from Mac App Store using {command}`mas`.
When this option is used, `"mas"` is automatically added to
[](#opt-homebrew.brews).
Note that you need to be signed into the Mac App Store for {command}`mas` to
successfully install and upgrade applications, and that unfortunately apps removed from this
option will not be uninstalled automatically even if
[](#opt-homebrew.onActivation.cleanup) is set to `"uninstall"`
or `"zap"` (this is currently a limitation of Homebrew Bundle).
For more information on {command}`mas` see:
[github.com/mas-cli/mas](https://github.com/mas-cli/mas).
'';
};
whalebrews = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "whalebrew/wget" ];
description = ''
List of Docker images to install using {command}`whalebrew`.
When this option is used, `"whalebrew"` is automatically added to
[](#opt-homebrew.brews).
For more information on {command}`whalebrew` see:
[github.com/whalebrew/whalebrew](https://github.com/whalebrew/whalebrew).
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
# '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 bottom of the generated Brewfile.";
};
brewfile = mkInternalOption {
type = types.str;
description = "String reprensentation of the generated Brewfile useful for debugging.";
};
};
# Implementation ---------------------------------------------------------------------------------
config = {
assertions = [
# See comment above `homebrew.global.noLock` option declaration for why this is required.
{ assertion = cfg.global.noLock == null; message = "The option `homebrew.global.noLock' was removed, use `homebrew.global.lockfiles' in it's place."; }
];
warnings = [
(mkIf (options.homebrew.autoUpdate.isDefined || options.homebrew.cleanup.isDefined) "The `homebrew' module no longer upgrades outdated formulae and apps by default during `nix-darwin' system activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.")
];
homebrew.brews =
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);
home.sessionVariables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables;
home.activation.homebrew = mkIf cfg.enable ''
# Homebrew Bundle
echo >&2 "Homebrew bundle..."
if [ -f "${cfg.brewPrefix}/brew" ]; then
PATH="${cfg.brewPrefix}":$PATH ${cfg.onActivation.brewBundleCmd}
else
echo -e "\e[1;31merror: Homebrew is not installed, skipping...\e[0m" >&2
fi
'';
};
}
# Created by: https://github.com/malob
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.homebrew;
brewfileFile = pkgs.writeText "Brewfile" cfg.brewfile;
# 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
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}: ${v}") attrs);
# Option and submodule helper functions ----------------------------------------------------------
mkNullOrBoolOption = args: mkOption (args // {
type = types.nullOr types.bool;
default = null;
});
mkNullOrStrOption = args: mkOption (args // {
type = types.nullOr types.str;
default = null;
});
mkInternalOption = args: mkOption (args // {
visible = false;
internal = true;
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
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 {command}`nix-darwin` system
activation.
When set to `"none"` (the default), formulae not present in the generated
Brewfile are left installed.
When set to `"uninstall"`, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--cleanup` flag. This
uninstalls all formulae not listed in generated Brewfile, i.e.,
{command}`brew uninstall` is run for those formulae.
When set to `"zap"`, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--cleanup --zap`
flags. This uninstalls all formulae not listed in the generated Brewfile, and if the
formula is a cask, removes all files associated with that cask. In other words,
{command}`brew uninstall --zap` is run for all those formulae.
If you plan on exclusively using {command}`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
{command}`nix-darwin` system activation. The default is `false`
so that repeated invocations of {command}`darwin-rebuild switch` are idempotent.
Note that Homebrew auto-updates when it's been more then 5 minutes since it last updated.
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 [](#opt-homebrew.global.autoUpdate).
Implementation note: when disabled, this option sets the `HOMEBREW_NO_AUTO_UPDATE`
environment variable when {command}`nix-darwin` invokes {command}`brew bundle [install]`
during system activation.
'';
};
upgrade = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Homebrew to upgrade outdated formulae and Mac App Store apps during
{command}`nix-darwin` system activation. The default is `false`
so that repeated invocations of {command}`darwin-rebuild switch` are idempotent.
Implementation note: when disabled, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--no-upgrade` flag during system
activation.
'';
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--verbose" ];
description = ''
Extra flags to pass to {command}`brew bundle [install]` during {command}`nix-darwin`
system activation.
'';
};
brewBundleCmd = mkInternalOption { type = types.str; };
};
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"
++ config.extraFlags
);
};
};
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 {command}`brew bundle`.
Enabling this option will change the default value of
[](#opt-homebrew.global.lockfiles) to `false` since, with
this option enabled, {command}`brew bundle [install]` will default to using the
Brewfile that this module generates in the Nix store, unless you explicitly point it at
another Brewfile using the `--file` flag. As a result, it will try to
write the lockfile in the Nix store, and complain that it can't (though the command will
run successfully regardless).
Implementation note: when enabled, this option sets the
`HOMEBREW_BUNDLE_FILE` environment variable to the path of the Brewfile
that this module generates in the Nix store, by adding it to
[](#opt-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 {command}`brew install`, {command}`brew upgrade`,
{command}`brew tap`, and {command}`brew bundle [install]`.
Note that Homebrew auto-updates when you manually invoke commands like the ones mentioned
above if it's been more then 5 minutes since it last updated.
You may want to consider disabling this option if you have
[](#opt-homebrew.onActivation.upgrade) enabled, and
[](#opt-homebrew.onActivation.autoUpdate) disabled, if you want to ensure that
your installed formulae will only be upgraded during {command}`nix-darwin` system
activation, after you've explicitly run {command}`brew update`.
Implementation note: when disabled, this option sets the
`HOMEBREW_NO_AUTO_UPDATE` environment variable, by adding it to
[](#opt-environment.variables).
'';
};
lockfiles = mkOption {
type = types.bool;
default = !config.brewfile;
defaultText = literalExpression "!config.homebrew.global.brewfile";
description = ''
Whether to enable Homebrew to generate lockfiles when you manually invoke
{command}`brew bundle [install]`.
This option will default to `false` if
[](#opt-homebrew.global.brewfile) is enabled since, with that option enabled,
{command}`brew bundle [install]` will default to using the Brewfile that this
module generates in the Nix store, unless you explicitly point it at another Brewfile
using the `--file` flag. As a result, it will try to write the
lockfile in the Nix store, and complain that it can't (though the command will run
successfully regardless).
Implementation note: when disabled, this option sets the
`HOMEBREW_BUNDLE_NO_LOCK` environment variable, by adding it to
[](#opt-environment.variables).
'';
};
# The `noLock` option was replaced by `lockfiles`. Due to `homebrew.global` being a submodule,
# we can't use `mkRemovedOptionModule`, so we leave this option definition here, and trigger
# and error message with an assertion below if it's set by the user.
noLock = mkOption { visible = false; default = null; };
homebrewEnvironmentVariables = mkInternalOption { type = types.attrs; };
};
config = {
homebrewEnvironmentVariables = mkMerge [
(mkIf config.brewfile { HOMEBREW_BUNDLE_FILE = "${brewfileFile}"; })
(mkIf (!config.autoUpdate) { HOMEBREW_NO_AUTO_UPDATE = "1"; })
(mkIf (!config.lockfiles) { HOMEBREW_BUNDLE_NO_LOCK = "1"; })
{ }
];
};
};
tapOptions = { config, ... }: {
options = {
name = mkOption {
type = types.str;
example = "homebrew/cask-fonts";
description = ''
When {option}`clone_target` 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 {command}`git` handles. When {option}`clone_target` 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 = mkInternalOption { type = types.nullOr types.str; };
};
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
# and valid values for `HOMEBREW_CASK_OPTS`.
caskArgsOptions = { config, ... }: {
options = {
appdir = mkNullOrStrOption {
description = ''
Target location for Applications.
Homebrew's default is {file}`/Applications`.
'';
};
colorpickerdir = mkNullOrStrOption {
description = ''
Target location for Color Pickers.
Homebrew's default is {file}`~/Library/ColorPickers`.
'';
};
prefpanedir = mkNullOrStrOption {
description = ''
Target location for Preference Panes.
Homebrew's default is {file}`~/Library/PreferencePanes`.
'';
};
qlplugindir = mkNullOrStrOption {
description = ''
Target location for QuickLook Plugins.
Homebrew's default is {file}`~/Library/QuickLook`.
'';
};
mdimporterdir = mkNullOrStrOption {
description = ''
Target location for Spotlight Plugins.
Homebrew's default is {file}`~/Library/Spotlight`.
'';
};
dictionarydir = mkNullOrStrOption {
description = ''
Target location for Dictionaries.
Homebrew's default is {file}`~/Library/Dictionaries`.
'';
};
fontdir = mkNullOrStrOption {
description = ''
Target location for Fonts.
Homebrew's default is {file}`~/Library/Fonts`.
'';
};
servicedir = mkNullOrStrOption {
description = ''
Target location for Services.
Homebrew's default is {file}`~/Library/Services`.
'';
};
input_methoddir = mkNullOrStrOption {
description = ''
Target location for Input Methods.
Homebrew's default is {file}`~/Library/Input Methods`.
'';
};
internet_plugindir = mkNullOrStrOption {
description = ''
Target location for Internet Plugins.
Homebrew's default is {file}`~/Library/Internet Plug-Ins`.
'';
};
audio_unit_plugindir = mkNullOrStrOption {
description = ''
Target location for Audio Unit Plugins.
Homebrew's default is
{file}`~/Library/Audio/Plug-Ins/Components`.
'';
};
vst_plugindir = mkNullOrStrOption {
description = ''
Target location for VST Plugins.
Homebrew's default is {file}`~/Library/Audio/Plug-Ins/VST`.
'';
};
vst3_plugindir = mkNullOrStrOption {
description = ''
Target location for VST3 Plugins.
Homebrew's default is {file}`~/Library/Audio/Plug-Ins/VST3`.
'';
};
screen_saverdir = mkNullOrStrOption {
description = ''
Target location for Screen Savers.
Homebrew's default is {file}`~/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.
Homebrew's default is `false`.
'';
};
no_quarantine = mkNullOrBoolOption {
description = "Whether to disable quarantining of downloads.";
};
no_binaries = mkNullOrBoolOption {
description = "Whether to disable linking of helper executables.";
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
in
{
brewfileLine =
if sCfg == { } then null
else "cask_args ${mkBrewfileLineOptionsListString sCfg}";
};
};
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 flags to pass to {command}`brew install`. Values should not include the
leading `"--"`.
'';
};
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 {command}`brew services restart` for the formula and register it to
launch at login (or boot). If set to `"changed"`, the service will only
be restarted on version changes.
Homebrew's default is `false`.
'';
};
start_service = mkNullOrBoolOption {
description = ''
Whether to run {command}`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 if it's currently unlinked and not keg-only, and to unlink the formula if it's
currently linked and keg-only.
'';
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
sCfgSubset = removeAttrs sCfg [ "name" "restart_service" ];
in
{
brewfileLine =
"brew ${sCfg.name}"
+ optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}"
# We need to handle the `restart_service` option seperately since it can be either a bool
# or `:changed` in the Brewfile.
+ optionalString (sCfg ? restart_service) (
", restart_service: " + (
if isBool config.restart_service then sCfg.restart_service
else ":${config.restart_service}"
)
);
};
};
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;
visible = "shallow"; # so that options from `homebrew.caskArgs` aren't repeated.
description = ''
Arguments passed to {command}`brew install --cask` when installing this cask. See
[](#opt-homebrew.caskArgs) for the available options.
'';
};
greedy = mkNullOrBoolOption {
description = ''
Whether to always upgrade this cask regardless of whether it's unversioned or it updates
itself.
'';
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
sCfgSubset = removeAttrs sCfg [ "name" ];
in
{
brewfileLine =
"cask ${sCfg.name}"
+ optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}";
};
};
in
{
# Interface --------------------------------------------------------------------------------------
imports = [
(mkRenamedOptionModule [ "homebrew" "autoUpdate" ] [ "homebrew" "onActivation" "autoUpdate" ])
(mkRenamedOptionModule [ "homebrew" "cleanup" ] [ "homebrew" "onActivation" "cleanup" ])
];
options.homebrew = {
enable = mkEnableOption ''
{command}`nix-darwin` to manage installing/updating/upgrading Homebrew taps, formulae,
and casks, as well as Mac App Store apps and Docker containers, using Homebrew Bundle.
Note that enabling this option does not install Homebrew, see the Homebrew
[website](https://brew.sh) for installation instructions.
Use the [](#opt-homebrew.brews), [](#opt-homebrew.casks),
[](#opt-homebrew.masApps), and [](#opt-homebrew.whalebrews) options
to list the Homebrew formulae, casks, Mac App Store apps, and Docker containers you'd like to
install. Use the [](#opt-homebrew.taps) option, to make additional formula
repositories available to Homebrew. This module uses those options (along with the
[](#opt-homebrew.caskArgs) options) to generate a Brewfile that
{command}`nix-darwin` passes to the {command}`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 {command}`darwin-rebuild switch` (without any change to the
configuration) are idempotent. You can modify this behavior using the options under
[](#opt-homebrew.onActivation).
This module also provides a few options for modifying how Homebrew commands behave when
you manually invoke them, under [](#opt-homebrew.global)'';
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 = ''
The path prefix where the {command}`brew` executable is located. This will be set to
the correct value based on your system's platform, and should only need to be changed if you
manually installed Homebrew in a non-standard location.
'';
};
onActivation = mkOption {
type = types.submodule onActivationOptions;
default = { };
description = ''
Options for configuring the behavior of the {command}`brew bundle` command that
{command}`nix-darwin` runs during system activation.
'';
};
global = mkOption {
type = types.submodule globalOptions;
default = { };
description = ''
Options for configuring the behavior of Homebrew commands when you manually invoke them.
'';
};
taps = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule tapOptions));
default = [ ];
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 = ''
List of Homebrew formula repositories to tap.
Taps defined as strings, e.g., `"user/repo"`, are a shorthand for:
`{ name = "user/repo"; }`
'';
};
caskArgs = mkOption {
type = types.submodule caskArgsOptions;
default = { };
example = literalExpression ''
{
appdir = "~/Applications";
require_sha = true;
}
'';
description = ''
Arguments passed to {command}`brew install --cask` for all casks listed in
[](#opt-homebrew.casks).
'';
};
brews = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule brewOptions));
default = [ ];
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 = ''
List of Homebrew formulae to install.
Formulae defined as strings, e.g., `"imagemagick"`, are a shorthand for:
`{ name = "imagemagick"; }`
'';
};
casks = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule caskOptions));
default = [ ];
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 = ''
List of Homebrew casks to install.
Casks defined as strings, e.g., `"google-chrome"`, are a shorthand for:
`{ name = "google-chrome"; }`
'';
};
masApps = mkOption {
type = types.attrsOf types.ints.positive;
default = { };
example = literalExpression ''
{
"1Password for Safari" = 1569813296;
Xcode = 497799835;
}
'';
description = ''
Applications to install from Mac App Store using {command}`mas`.
When this option is used, `"mas"` is automatically added to
[](#opt-homebrew.brews).
Note that you need to be signed into the Mac App Store for {command}`mas` to
successfully install and upgrade applications, and that unfortunately apps removed from this
option will not be uninstalled automatically even if
[](#opt-homebrew.onActivation.cleanup) is set to `"uninstall"`
or `"zap"` (this is currently a limitation of Homebrew Bundle).
For more information on {command}`mas` see:
[github.com/mas-cli/mas](https://github.com/mas-cli/mas).
'';
};
whalebrews = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "whalebrew/wget" ];
description = ''
List of Docker images to install using {command}`whalebrew`.
When this option is used, `"whalebrew"` is automatically added to
[](#opt-homebrew.brews).
For more information on {command}`whalebrew` see:
[github.com/whalebrew/whalebrew](https://github.com/whalebrew/whalebrew).
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
# '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 bottom of the generated Brewfile.";
};
brewfile = mkInternalOption {
type = types.str;
description = "String reprensentation of the generated Brewfile useful for debugging.";
};
};
# Implementation ---------------------------------------------------------------------------------
config = {
assertions = [
# See comment above `homebrew.global.noLock` option declaration for why this is required.
{ assertion = cfg.global.noLock == null; message = "The option `homebrew.global.noLock' was removed, use `homebrew.global.lockfiles' in it's place."; }
];
warnings = [
(mkIf (options.homebrew.autoUpdate.isDefined || options.homebrew.cleanup.isDefined) "The `homebrew' module no longer upgrades outdated formulae and apps by default during `nix-darwin' system activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.")
];
homebrew.brews =
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);
home.sessionVariables = mkIf cfg.enable cfg.global.homebrewEnvironmentVariables;
home.activation.homebrew = mkIf cfg.enable ''
# Homebrew Bundle
echo >&2 "Homebrew bundle..."
if [ -f "${cfg.brewPrefix}/brew" ]; then
PATH="${cfg.brewPrefix}":$PATH ${cfg.onActivation.brewBundleCmd}
else
echo -e "\e[1;31merror: Homebrew is not installed, skipping...\e[0m" >&2
fi
'';
};
}
# Created by: https://github.com/malob
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.homebrew;
brewfileFile = pkgs.writeText "Brewfile" cfg.brewfile;
# 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
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}: ${v}") attrs);
# Option and submodule helper functions ----------------------------------------------------------
mkNullOrBoolOption = args: mkOption (args // {
type = types.nullOr types.bool;
default = null;
});
mkNullOrStrOption = args: mkOption (args // {
type = types.nullOr types.str;
default = null;
});
mkInternalOption = args: mkOption (args // {
visible = false;
internal = true;
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
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 {command}`nix-darwin` system
activation.
When set to `"none"` (the default), formulae not present in the generated
Brewfile are left installed.
When set to `"uninstall"`, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--cleanup` flag. This
uninstalls all formulae not listed in generated Brewfile, i.e.,
{command}`brew uninstall` is run for those formulae.
When set to `"zap"`, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--cleanup --zap`
flags. This uninstalls all formulae not listed in the generated Brewfile, and if the
formula is a cask, removes all files associated with that cask. In other words,
{command}`brew uninstall --zap` is run for all those formulae.
If you plan on exclusively using {command}`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
{command}`nix-darwin` system activation. The default is `false`
so that repeated invocations of {command}`darwin-rebuild switch` are idempotent.
Note that Homebrew auto-updates when it's been more then 5 minutes since it last updated.
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 [](#opt-homebrew.global.autoUpdate).
Implementation note: when disabled, this option sets the `HOMEBREW_NO_AUTO_UPDATE`
environment variable when {command}`nix-darwin` invokes {command}`brew bundle [install]`
during system activation.
'';
};
upgrade = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Homebrew to upgrade outdated formulae and Mac App Store apps during
{command}`nix-darwin` system activation. The default is `false`
so that repeated invocations of {command}`darwin-rebuild switch` are idempotent.
Implementation note: when disabled, {command}`nix-darwin` invokes
{command}`brew bundle [install]` with the {command}`--no-upgrade` flag during system
activation.
'';
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--verbose" ];
description = ''
Extra flags to pass to {command}`brew bundle [install]` during {command}`nix-darwin`
system activation.
'';
};
brewBundleCmd = mkInternalOption { type = types.str; };
};
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"
++ config.extraFlags
);
};
};
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 {command}`brew bundle`.
Enabling this option will change the default value of
[](#opt-homebrew.global.lockfiles) to `false` since, with
this option enabled, {command}`brew bundle [install]` will default to using the
Brewfile that this module generates in the Nix store, unless you explicitly point it at
another Brewfile using the `--file` flag. As a result, it will try to
write the lockfile in the Nix store, and complain that it can't (though the command will
run successfully regardless).
Implementation note: when enabled, this option sets the
`HOMEBREW_BUNDLE_FILE` environment variable to the path of the Brewfile
that this module generates in the Nix store, by adding it to
[](#opt-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 {command}`brew install`, {command}`brew upgrade`,
{command}`brew tap`, and {command}`brew bundle [install]`.
Note that Homebrew auto-updates when you manually invoke commands like the ones mentioned
above if it's been more then 5 minutes since it last updated.
You may want to consider disabling this option if you have
[](#opt-homebrew.onActivation.upgrade) enabled, and
[](#opt-homebrew.onActivation.autoUpdate) disabled, if you want to ensure that
your installed formulae will only be upgraded during {command}`nix-darwin` system
activation, after you've explicitly run {command}`brew update`.
Implementation note: when disabled, this option sets the
`HOMEBREW_NO_AUTO_UPDATE` environment variable, by adding it to
[](#opt-environment.variables).
'';
};
lockfiles = mkOption {
type = types.bool;
default = !config.brewfile;
defaultText = literalExpression "!config.homebrew.global.brewfile";
description = ''
Whether to enable Homebrew to generate lockfiles when you manually invoke
{command}`brew bundle [install]`.
This option will default to `false` if
[](#opt-homebrew.global.brewfile) is enabled since, with that option enabled,
{command}`brew bundle [install]` will default to using the Brewfile that this
module generates in the Nix store, unless you explicitly point it at another Brewfile
using the `--file` flag. As a result, it will try to write the
lockfile in the Nix store, and complain that it can't (though the command will run
successfully regardless).
Implementation note: when disabled, this option sets the
`HOMEBREW_BUNDLE_NO_LOCK` environment variable, by adding it to
[](#opt-environment.variables).
'';
};
# The `noLock` option was replaced by `lockfiles`. Due to `homebrew.global` being a submodule,
# we can't use `mkRemovedOptionModule`, so we leave this option definition here, and trigger
# and error message with an assertion below if it's set by the user.
noLock = 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 {
type = types.str;
example = "homebrew/cask-fonts";
description = ''
When {option}`clone_target` 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 {command}`git` handles. When {option}`clone_target` 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 = mkInternalOption { type = types.nullOr types.str; };
};
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
# and valid values for `HOMEBREW_CASK_OPTS`.
caskArgsOptions = { config, ... }: {
options = {
appdir = mkNullOrStrOption {
description = ''
Target location for Applications.
Homebrew's default is {file}`/Applications`.
'';
};
colorpickerdir = mkNullOrStrOption {
description = ''
Target location for Color Pickers.
Homebrew's default is {file}`~/Library/ColorPickers`.
'';
};
prefpanedir = mkNullOrStrOption {
description = ''
Target location for Preference Panes.
Homebrew's default is {file}`~/Library/PreferencePanes`.
'';
};
qlplugindir = mkNullOrStrOption {
description = ''
Target location for QuickLook Plugins.
Homebrew's default is {file}`~/Library/QuickLook`.
'';
};
mdimporterdir = mkNullOrStrOption {
description = ''
Target location for Spotlight Plugins.
Homebrew's default is {file}`~/Library/Spotlight`.
'';
};
dictionarydir = mkNullOrStrOption {
description = ''
Target location for Dictionaries.
Homebrew's default is {file}`~/Library/Dictionaries`.
'';
};
fontdir = mkNullOrStrOption {
description = ''
Target location for Fonts.
Homebrew's default is {file}`~/Library/Fonts`.
'';
};
servicedir = mkNullOrStrOption {
description = ''
Target location for Services.
Homebrew's default is {file}`~/Library/Services`.
'';
};
input_methoddir = mkNullOrStrOption {
description = ''
Target location for Input Methods.
Homebrew's default is {file}`~/Library/Input Methods`.
'';
};
internet_plugindir = mkNullOrStrOption {
description = ''
Target location for Internet Plugins.
Homebrew's default is {file}`~/Library/Internet Plug-Ins`.
'';
};
audio_unit_plugindir = mkNullOrStrOption {
description = ''
Target location for Audio Unit Plugins.
Homebrew's default is
{file}`~/Library/Audio/Plug-Ins/Components`.
'';
};
vst_plugindir = mkNullOrStrOption {
description = ''
Target location for VST Plugins.
Homebrew's default is {file}`~/Library/Audio/Plug-Ins/VST`.
'';
};
vst3_plugindir = mkNullOrStrOption {
description = ''
Target location for VST3 Plugins.
Homebrew's default is {file}`~/Library/Audio/Plug-Ins/VST3`.
'';
};
screen_saverdir = mkNullOrStrOption {
description = ''
Target location for Screen Savers.
Homebrew's default is {file}`~/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.
Homebrew's default is `false`.
'';
};
no_quarantine = mkNullOrBoolOption {
description = "Whether to disable quarantining of downloads.";
};
no_binaries = mkNullOrBoolOption {
description = "Whether to disable linking of helper executables.";
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
in
{
brewfileLine =
if sCfg == { } then null
else "cask_args ${mkBrewfileLineOptionsListString sCfg}";
};
};
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 flags to pass to {command}`brew install`. Values should not include the
leading `"--"`.
'';
};
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 {command}`brew services restart` for the formula and register it to
launch at login (or boot). If set to `"changed"`, the service will only
be restarted on version changes.
Homebrew's default is `false`.
'';
};
start_service = mkNullOrBoolOption {
description = ''
Whether to run {command}`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 if it's currently unlinked and not keg-only, and to unlink the formula if it's
currently linked and keg-only.
'';
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
sCfgSubset = removeAttrs sCfg [ "name" "restart_service" ];
in
{
brewfileLine =
"brew ${sCfg.name}"
+ optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}"
# We need to handle the `restart_service` option seperately since it can be either a bool
# or `:changed` in the Brewfile.
+ optionalString (sCfg ? restart_service) (
", restart_service: " + (
if isBool config.restart_service then sCfg.restart_service
else ":${config.restart_service}"
)
);
};
};
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;
visible = "shallow"; # so that options from `homebrew.caskArgs` aren't repeated.
description = ''
Arguments passed to {command}`brew install --cask` when installing this cask. See
[](#opt-homebrew.caskArgs) for the available options.
'';
};
greedy = mkNullOrBoolOption {
description = ''
Whether to always upgrade this cask regardless of whether it's unversioned or it updates
itself.
'';
};
brewfileLine = mkInternalOption { type = types.nullOr types.str; };
};
config =
let
sCfg = mkProcessedSubmodConfig config;
sCfgSubset = removeAttrs sCfg [ "name" ];
in
{
brewfileLine =
"cask ${sCfg.name}"
+ optionalString (sCfgSubset != { }) ", ${mkBrewfileLineOptionsListString sCfgSubset}";
};
};
in
{
# Interface --------------------------------------------------------------------------------------
imports = [
(mkRenamedOptionModule [ "homebrew" "autoUpdate" ] [ "homebrew" "onActivation" "autoUpdate" ])
(mkRenamedOptionModule [ "homebrew" "cleanup" ] [ "homebrew" "onActivation" "cleanup" ])
];
options.homebrew = {
enable = mkEnableOption ''
{command}`nix-darwin` to manage installing/updating/upgrading Homebrew taps, formulae,
and casks, as well as Mac App Store apps and Docker containers, using Homebrew Bundle.
Note that enabling this option does not install Homebrew, see the Homebrew
[website](https://brew.sh) for installation instructions.
Use the [](#opt-homebrew.brews), [](#opt-homebrew.casks),
[](#opt-homebrew.masApps), and [](#opt-homebrew.whalebrews) options
to list the Homebrew formulae, casks, Mac App Store apps, and Docker containers you'd like to
install. Use the [](#opt-homebrew.taps) option, to make additional formula
repositories available to Homebrew. This module uses those options (along with the
[](#opt-homebrew.caskArgs) options) to generate a Brewfile that
{command}`nix-darwin` passes to the {command}`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 {command}`darwin-rebuild switch` (without any change to the
configuration) are idempotent. You can modify this behavior using the options under
[](#opt-homebrew.onActivation).
This module also provides a few options for modifying how Homebrew commands behave when
you manually invoke them, under [](#opt-homebrew.global)'';
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 = ''
The path prefix where the {command}`brew` executable is located. This will be set to
the correct value based on your system's platform, and should only need to be changed if you
manually installed Homebrew in a non-standard location.
'';
};
onActivation = mkOption {
type = types.submodule onActivationOptions;
default = { };
description = ''
Options for configuring the behavior of the {command}`brew bundle` command that
{command}`nix-darwin` runs during system activation.
'';
};
global = mkOption {
type = types.submodule globalOptions;
default = { };
description = ''
Options for configuring the behavior of Homebrew commands when you manually invoke them.
'';
};
taps = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule tapOptions));
default = [ ];
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 = ''
List of Homebrew formula repositories to tap.
Taps defined as strings, e.g., `"user/repo"`, are a shorthand for:
`{ name = "user/repo"; }`
'';
};
caskArgs = mkOption {
type = types.submodule caskArgsOptions;
default = { };
example = literalExpression ''
{
appdir = "~/Applications";
require_sha = true;
}
'';
description = ''
Arguments passed to {command}`brew install --cask` for all casks listed in
[](#opt-homebrew.casks).
'';
};
brews = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule brewOptions));
default = [ ];
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 = ''
List of Homebrew formulae to install.
Formulae defined as strings, e.g., `"imagemagick"`, are a shorthand for:
`{ name = "imagemagick"; }`
'';
};
casks = mkOption {
type = with types; listOf (coercedTo str (name: { inherit name; }) (submodule caskOptions));
default = [ ];
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 = ''
List of Homebrew casks to install.
Casks defined as strings, e.g., `"google-chrome"`, are a shorthand for:
`{ name = "google-chrome"; }`
'';
};
masApps = mkOption {
type = types.attrsOf types.ints.positive;
default = { };
example = literalExpression ''
{
"1Password for Safari" = 1569813296;
Xcode = 497799835;
}
'';
description = ''
Applications to install from Mac App Store using {command}`mas`.
When this option is used, `"mas"` is automatically added to
[](#opt-homebrew.brews).
Note that you need to be signed into the Mac App Store for {command}`mas` to
successfully install and upgrade applications, and that unfortunately apps removed from this
option will not be uninstalled automatically even if
[](#opt-homebrew.onActivation.cleanup) is set to `"uninstall"`
or `"zap"` (this is currently a limitation of Homebrew Bundle).
For more information on {command}`mas` see:
[github.com/mas-cli/mas](https://github.com/mas-cli/mas).
'';
};
whalebrews = mkOption {
type = with types; listOf str;
default = [ ];
example = [ "whalebrew/wget" ];
description = ''
List of Docker images to install using {command}`whalebrew`.
When this option is used, `"whalebrew"` is automatically added to
[](#opt-homebrew.brews).
For more information on {command}`whalebrew` see:
[github.com/whalebrew/whalebrew](https://github.com/whalebrew/whalebrew).
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
example = ''
# '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 bottom of the generated Brewfile.";
};
brewfile = mkInternalOption {
type = types.str;
description = "String reprensentation of the generated Brewfile useful for debugging.";
};
};
# Implementation ---------------------------------------------------------------------------------
config = {
assertions = [
# See comment above `homebrew.global.noLock` option declaration for why this is required.
{ assertion = cfg.global.noLock == null; message = "The option `homebrew.global.noLock' was removed, use `homebrew.global.lockfiles' in it's place."; }
];
warnings = [
(mkIf (options.homebrew.autoUpdate.isDefined || options.homebrew.cleanup.isDefined) "The `homebrew' module no longer upgrades outdated formulae and apps by default during `nix-darwin' system activation. To enable upgrading, set `homebrew.onActivation.upgrade = true'.")
];
homebrew.brews =
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 cfg.global.homebrewEnvironmentVariables;
system.activationScripts.homebrew.text = mkIf cfg.enable ''
# Homebrew Bundle
echo >&2 "Homebrew bundle..."
if [ -f "${cfg.brewPrefix}/brew" ]; then
PATH="${cfg.brewPrefix}":$PATH ${cfg.onActivation.brewBundleCmd}
else
echo -e "\e[1;31merror: Homebrew is not installed, skipping...\e[0m" >&2
fi
'';
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment