Files
home-manager/modules/programs/firefox/mkFirefoxModule.nix
Ihar Hrachyshka 922a645e84 firefox: apply configPath to package wrapper
This is a counterpart for: https://github.com/NixOS/nixpkgs/pull/556611

When configPath is overridden, if wrapper supports the new appDataDir
argument, we pass configPath's absolute form to tell the app to look for
its datadir in the correct directory.

This will become necessary on macOS 27+ where Firefox that is not signed
by Mozilla has no access to its default datadir.
2026-09-10 22:57:58 -05:00

1403 lines
50 KiB
Nix

{
modulePath,
name,
description ? null,
wrappedPackageName ? null,
unwrappedPackageName ? null,
platforms,
enableBookmarks ? true,
}:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
attrValues
concatStringsSep
length
literalExpression
mapAttrsToList
mkDefault
mkIf
mkMerge
mkOption
mkOptionDefault
optionalString
optional
setAttrByPath
types
;
inherit (pkgs.stdenv.hostPlatform) isDarwin;
defaultConfigPath = with platforms; if isDarwin then darwin.configPath else linux.configPath;
appName = name;
moduleName = concatStringsSep "." modulePath;
cfg = lib.getAttrFromPath modulePath config;
jsonFormat = pkgs.formats.json { };
supportedPlatforms = lib.flatten (lib.attrVals (lib.attrNames platforms) lib.platforms);
isWrapped = lib.versionAtLeast config.home.stateVersion "19.09" && wrappedPackageName != null;
defaultPackageName = if isWrapped then wrappedPackageName else unwrappedPackageName;
packageName = if wrappedPackageName != null then wrappedPackageName else unwrappedPackageName;
# The extensions path shared by all profiles; will not be supported
# by future browser versions.
extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
profiles =
lib.flip lib.mapAttrs' cfg.profiles (
_: profile:
lib.nameValuePair "Profile${toString profile.id}" (
{
Name = profile.name;
Path = if isDarwin then "Profiles/${profile.path}" else profile.path;
IsRelative = 1;
Default = if profile.isDefault then 1 else 0;
}
// (lib.optionalAttrs (profile.storeId != null) {
StoreID = profile.storeId;
})
)
)
// {
General = {
StartWithLastProfile = 1;
}
// lib.optionalAttrs (cfg.profileVersion != null) {
Version = cfg.profileVersion;
};
};
profilesIni = lib.generators.toINI { } profiles;
userPrefValue =
pref:
builtins.toJSON (
if lib.isBool pref || lib.isInt pref || lib.isString pref || lib.isPath pref then
pref
else
builtins.toJSON pref
);
extensionSettingsNeedForce =
extensionSettings: builtins.any (ext: ext.settings != { }) (attrValues extensionSettings);
extensionSettingsMissingForce =
extensionSettings:
builtins.any (ext: ext.settings != { } && !ext.force) (attrValues extensionSettings);
mkUserJs =
prePrefs: prefs: extraPrefs: bookmarksFile: extensions:
let
prefs' =
lib.optionalAttrs (bookmarksFile != null) {
"browser.bookmarks.file" = toString bookmarksFile;
"browser.places.importBookmarksHTML" = true;
}
// lib.optionalAttrs (extensionSettingsNeedForce extensions) {
"extensions.webextensions.ExtensionStorageIDB.enabled" = false;
}
// prefs;
in
''
// Generated by Home Manager.
${prePrefs}
${lib.concatStrings (
mapAttrsToList (name: value: ''
user_pref("${name}", ${userPrefValue value});
'') prefs'
)}
${extraPrefs}
'';
mkContainersJson =
containers:
let
containerToIdentity = _: container: {
userContextId = container.id;
inherit (container) color icon name;
public = true;
};
in
''
${builtins.toJSON {
version = 5;
lastUserContextId = lib.foldlAttrs (
acc: _: value:
if value.id > acc then value.id else acc
) 0 containers;
identities = mapAttrsToList containerToIdentity containers ++ [
{
userContextId = 4294967294; # 2^32 - 2
name = "userContextIdInternal.thumbnail";
icon = "";
color = "";
accessKey = "";
public = false;
}
{
userContextId = 4294967295; # 2^32 - 1
name = "userContextIdInternal.webextStorageLocal";
icon = "";
color = "";
accessKey = "";
public = false;
}
];
}}
'';
mkNoDuplicateAssertion =
entities: entityKind:
(
let
# Return an attribute set with entity IDs as keys and a list of
# entity names with corresponding ID as value. An ID is present in
# the result only if more than one entity has it. The argument
# entities is a list of AttrSet of one id/name pair.
findDuplicateIds =
entities: lib.filterAttrs (_entityId: entityNames: length entityNames != 1) (lib.zipAttrs entities);
duplicates = findDuplicateIds (
mapAttrsToList (entityName: entity: { "${toString entity.id}" = entityName; }) entities
);
mkMsg = entityId: entityNames: " - ID ${entityId} is used by " + concatStringsSep ", " entityNames;
in
{
assertion = duplicates == { };
message = ''
Must not have a ${appName} ${entityKind} with an existing ID but
''
+ concatStringsSep "\n" (mapAttrsToList mkMsg duplicates);
}
);
wrapPackage =
package:
let
# The configuration expected by the Firefox wrapper.
fcfg = {
inherit (cfg) enableGnomeExtensions;
};
# A bit of hackery to force a config into the wrapper.
browserName = package.browserName or (builtins.parseDrvName package.name).name;
# The configuration expected by the Firefox wrapper builder.
bcfg = setAttrByPath [ browserName ] fcfg;
configureAppDataDir = cfg.configPath != defaultConfigPath;
absoluteConfigPath =
if lib.hasPrefix "/" cfg.configPath then
cfg.configPath
else
"${config.home.homeDirectory}/${cfg.configPath}";
callWithAppDataDir =
wrapper: args:
let
wrapperArgs = lib.functionArgs wrapper;
supportsAppDataDir = wrapperArgs ? appDataDir;
# Gracefully handle wrappers that don't, yet, support appDataDir.
appDataDirArgs = lib.optionalAttrs (configureAppDataDir && supportsAppDataDir) {
appDataDir = absoluteConfigPath;
};
argsWithAppDataDir =
if lib.isFunction args then
# Modern wrapped packages pass an override function.
old: args old // appDataDirArgs
else
# Legacy wrapFirefox passes an attribute set.
args // appDataDirArgs;
in
lib.warnIf (configureAppDataDir && !supportsAppDataDir)
"${moduleName}: '${browserName}' does not support the 'appDataDir' wrapper argument; 'configPath' will not be applied to the package wrapper."
(wrapper argsWithAppDataDir);
in
if package == null then
null
else if isWrapped then
if lib.functionArgs package.override ? cfg then
callWithAppDataDir package.override (old: {
cfg = old.cfg or { } // fcfg;
extraPolicies = (old.extraPolicies or { }) // cfg.policies;
pkcs11Modules = (old.pkcs11Modules or [ ]) ++ cfg.pkcs11Modules;
})
else
let
droppedPolicies = cfg.policies != { } && (!isDarwin || cfg.darwinDefaultsId == null);
droppedOptions =
droppedPolicies || cfg.pkcs11Modules != [ ] || cfg.enableGnomeExtensions || configureAppDataDir;
in
lib.warnIf droppedOptions
"${moduleName}: '${browserName}' cannot be reconfigured; 'policies', 'pkcs11Modules', 'enableGnomeExtensions', and 'configPath' will not be applied."
package
else
callWithAppDataDir ((pkgs.wrapFirefox.override { config = bcfg; }) package) { };
bookmarkTypes = import ./profiles/bookmark-types.nix { inherit lib; };
in
{
options = setAttrByPath modulePath {
enable = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
Whether to enable ${appName}.${optionalString (description != null) " ${description}"}
'';
};
package = mkOption {
type = with types; nullOr package;
default = pkgs.${defaultPackageName};
defaultText = literalExpression "pkgs.${packageName}";
example = literalExpression ''
pkgs.${packageName}.override {
# See nixpkgs' firefox/wrapper.nix to check which options you can use
nativeMessagingHosts = [
# Gnome shell native connector
pkgs.gnome-browser-connector
# Tridactyl native connector
pkgs.tridactyl-native
];
}
'';
description = ''
The ${appName} package to use. If state version 19.09 then
this should be a wrapped ${appName} package. For earlier state
versions it should be an unwrapped ${appName} package.
Set to `null` to disable installing ${appName}.
'';
};
release = mkOption {
internal = true;
type = types.str;
description = "Upstream release version used to fetch from `releases.mozilla.org`.";
};
globalExtensions = mkOption {
type = types.listOf (
types.oneOf [
types.package
(types.submodule {
options = {
package = mkOption {
type = types.package;
};
settings = mkOption {
type = types.attrsOf jsonFormat.type;
default = { };
description = "Json formatted options for this extension.";
};
};
})
]
);
default = [ ];
example = literalExpression ''
with pkgs.nur.repos.rycee.firefox-addons; [
privacy-badger
{
package = ublock-origin;
settings = {
private_browsing = true;
};
}
]
'';
description = ''
Add-on package to install under policies.
For a package to work here it needs and addonId exposed in it's passthru.
This will be included in all add-ons accessible from the Nix User Repository.
Once you have NUR installed run
```console
$ nix-env -f '<nixpkgs>' -qaP -A nur.repos.rycee.firefox-addons
```
to list the available ${name} add-ons.
Installing extensions this way will automatically enable extensions
inside ${name} after the first installation.
'';
};
languagePacks = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
The language packs to install. Available language codes can be found
on the releases page:
`https://releases.mozilla.org/pub/firefox/releases/''${version}/linux-x86_64/xpi/`,
replacing `''${version}` with the version of ${appName} you have. If
the version string of your Firefox derivative diverts from the upstream
version, try setting the `release` option.
'';
example = [
"en-GB"
"de"
];
};
name = mkOption {
internal = true;
type = types.str;
default = name;
example = "Firefox";
description = "The name of the browser.";
};
wrappedPackageName = mkOption {
internal = true;
type = with types; nullOr str;
default = wrappedPackageName;
description = "Name of the wrapped browser package.";
};
darwinDefaultsId = mkOption rec {
type = types.nullOr types.str;
default = if platforms.darwin ? "defaultsId" then platforms.darwin.defaultsId else null;
example = if default != null then default else "com.developer.app";
description = "The id for the darwin defaults in order to set policies";
};
darwinAppName = mkOption {
internal = true;
type = types.str;
default =
if platforms.darwin ? "appName" then
platforms.darwin.appName
else
lib.toUpper (lib.substring 0 1 cfg.wrappedPackageName)
+ lib.toLower (
lib.substring 1 ((lib.stringLength cfg.wrappedPackageName) - 1) cfg.wrappedPackageName
);
description = "Name of browser app on Darwin.";
};
profilesPath = mkOption {
internal = true;
type = types.str;
default = if isDarwin then "${cfg.configPath}/Profiles" else cfg.configPath;
description = "Path to profiles.";
};
vendorPath = mkOption {
internal = true;
type = with types; nullOr str;
default = null;
defaultText = literalExpression "platform specific vendor path";
example = ".mozilla";
description = "Directory containing the native messaging hosts directory.";
};
configPath = mkOption {
type = types.str;
default = defaultConfigPath;
defaultText = literalExpression "platform specific default config path";
example = ".mozilla/firefox";
description = ''
Directory containing the ${appName} configuration files. A relative
path is interpreted relative to the user's home directory.
Setting this to a non-default path also configures the package wrapper
to use it as the application data directory. This can be useful on
macOS 27 and later, where wrapped applications may be denied access to
the traditional application data directory.
'';
};
nativeMessagingHosts = mkOption {
type = types.listOf types.package;
default = [ ];
description = ''
Additional packages containing native messaging hosts that should be
made available to ${appName} extensions.
'';
};
finalPackage = mkOption {
type = with types; nullOr package;
readOnly = true;
description = "Resulting ${appName} package.";
};
policies = lib.optionalAttrs (wrappedPackageName != null) (mkOption {
type = types.attrsOf jsonFormat.type;
default = { };
description = "[See list of policies](https://mozilla.github.io/policy-templates/).";
example = {
DefaultDownloadDirectory = "\${home}/Downloads";
BlockAboutConfig = true;
ExtensionSettings = {
"uBlock0@raymondhill.net" = {
install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
installation_mode = "force_installed";
default_area = "menupanel";
private_browsing = true;
};
};
};
});
profileVersion = mkOption {
internal = true;
type = types.nullOr types.ints.unsigned;
default = if isDarwin then null else 2;
description = "profile version, set null for nix-darwin";
};
profiles = mkOption {
type = types.attrsOf (
types.submodule (
{
config,
name,
...
}:
let
profilePath = modulePath ++ [
"profiles"
name
];
in
{
imports = [ (pkgs.path + "/nixos/modules/misc/assertions.nix") ];
options = {
name = mkOption {
type = types.str;
default = name;
description = "Profile name.";
};
id = mkOption {
type = types.ints.unsigned;
default = 0;
description = ''
Profile ID. This should be set to a unique number per profile.
'';
};
storeId = mkOption {
type = types.nullOr (types.strMatching "[0-9a-f]{8}");
default = null;
example = "e41de5fe";
description = ''
Store ID. Either null, or the first segment of a UUID string (8 lowercase hex characters).
If this value is set, then profiles.ini is created with predictable StoreIDs.
'toolkit.profiles.storeID' is also set accordingly.
A predictable StoreID helps bridge the old and new firefox profile implementations.
The StoreID is the name of the sqlite database in "Profile Groups" holding the new
profiles' metadata.
'';
};
preConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra preferences to add to {file}`user.js`, before
[](#opt-programs.firefox.profiles._name_.settings).
Use [](#opt-programs.firefox.profiles._name_.extraConfig), unless
you want to overwrite in
[](#opt-programs.firefox.profiles._name_.settings), then use this
option.
'';
};
settings = mkOption {
type = types.attrsOf (
jsonFormat.type
// {
description = "${appName} preference (int, bool, string, and also attrs, list, float as a JSON string)";
}
);
default = { };
example = literalExpression ''
{
"browser.startup.homepage" = "https://nixos.org";
"browser.search.region" = "GB";
"browser.search.isUS" = false;
"distribution.searchplugins.defaultLocale" = "en-GB";
"general.useragent.locale" = "en-GB";
"browser.bookmarks.showMobileBookmarks" = true;
"browser.newtabpage.pinned" = [{
title = "NixOS";
url = "https://nixos.org";
}];
}
'';
description = ''
Attribute set of ${appName} preferences.
${appName} only supports int, bool, and string types for
preferences, but home-manager will automatically
convert all other JSON-compatible values into strings.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra preferences to add to {file}`user.js`.
'';
};
userChrome = mkOption {
type = types.oneOf [
types.lines
types.path
];
default = "";
description = "Custom ${appName} user chrome CSS.";
example = ''
/* Hide tab bar in FF Quantum */
@-moz-document url(chrome://browser/content/browser.xul), url(chrome://browser/content/browser.xhtml) {
#TabsToolbar {
visibility: collapse !important;
margin-bottom: 21px !important;
}
#sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header {
visibility: collapse !important;
}
}
'';
};
userContent = mkOption {
type = types.oneOf [
types.lines
types.path
];
default = "";
description = "Custom ${appName} user content CSS.";
example = ''
/* Hide scrollbar in FF Quantum */
*{scrollbar-width:none !important}
'';
};
bookmarks = mkOption {
type = (
types.coercedTo bookmarkTypes.settingsType
(
bookmarks:
if bookmarks != { } then
{
force = true;
_legacySettings = if builtins.isList bookmarks then "a list" else "an attribute set";
settings = bookmarks;
}
else
{ }
)
(
types.submodule (
{ config, ... }:
import ./profiles/bookmarks.nix {
inherit config lib pkgs;
modulePath = modulePath ++ [
"profiles"
name
"bookmarks"
];
}
)
)
);
default = { };
internal = !enableBookmarks;
description = "Declarative bookmarks.";
};
path = mkOption {
type = types.str;
default = name;
description = "Profile path.";
};
isDefault = mkOption {
type = types.bool;
default = config.id == 0;
defaultText = "true if profile ID is 0";
description = "Whether this is a default profile.";
};
search = mkOption {
type = types.submodule (
args:
import ./profiles/search.nix {
inherit (args) config;
inherit lib pkgs appName;
package = cfg.finalPackage;
modulePath = modulePath ++ [
"profiles"
name
"search"
];
profilePath = config.path;
}
);
default = { };
description = "Declarative search engine configuration.";
};
handlers = mkOption {
type = types.submodule (
args:
import ./profiles/handlers.nix {
inherit (args) config;
inherit lib pkgs appName;
package = cfg.finalPackage;
modulePath = modulePath ++ [
"profiles"
name
"handlers"
];
profilePath = config.path;
}
);
default = { };
description = "Declarative handlers configuration for MIME types and URL schemes.";
};
containersForce = mkOption {
type = types.bool;
default = false;
description = ''
Whether to force replace the existing containers configuration.
This is recommended since ${appName} will replace the symlink on
every launch, but note that you'll lose any existing configuration
by enabling this.
'';
};
containers = mkOption {
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
name = mkOption {
type = types.str;
default = name;
description = "Container name, e.g., shopping.";
};
id = mkOption {
type = types.ints.unsigned;
default = 1;
description = ''
Container ID. This should be set to a unique number per container in this profile.
'';
};
# List of colors at
# https://searchfox.org/mozilla-central/rev/5ad226c7379b0564c76dc3b54b44985356f94c5a/toolkit/components/extensions/parent/ext-contextualIdentities.js#32
color = mkOption {
type = types.enum [
"blue"
"turquoise"
"green"
"yellow"
"orange"
"red"
"pink"
"purple"
"toolbar"
];
default = "pink";
description = "Container color.";
};
icon = mkOption {
type = types.enum [
"briefcase"
"cart"
"circle"
"dollar"
"fence"
"fingerprint"
"gift"
"vacation"
"food"
"fruit"
"pet"
"tree"
"chill"
];
default = "fruit";
description = "Container icon.";
};
};
}
)
);
default = { };
example = {
"shopping" = {
id = 1;
color = "blue";
icon = "cart";
};
"dangerous" = {
id = 2;
color = "red";
icon = "fruit";
};
};
description = ''
Attribute set of container configurations. See
[Multi-Account
Containers](https://support.mozilla.org/en-US/kb/containers)
for more information.
'';
};
extensions = mkOption {
type = types.submodule {
options = {
packages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression ''
with pkgs.nur.repos.rycee.firefox-addons; [
privacy-badger
]
'';
description = ''
List of ${name} add-on packages to install for this profile.
Some pre-packaged add-ons are accessible from the Nix User Repository.
Once you have NUR installed run
```console
$ nix-env -f '<nixpkgs>' -qaP -A nur.repos.rycee.firefox-addons
```
to list the available ${name} add-ons.
Note that it is necessary to manually enable these extensions
inside ${name} after the first installation.
To automatically enable extensions add
`"extensions.autoDisableScopes" = 0;`
to
[{option}`${moduleName}.profiles.<profile>.settings`](#opt-${moduleName}.profiles._name_.settings)
On systems using impermanence, this only prevents
${name} from requiring manual extension approval. It
does not preserve extension runtime state such as
extension UUIDs, logins, local storage, or
per-extension data. Persist the ${name} profile state
needed by your extensions, or configure supported
extension settings declaratively with
[{option}`${moduleName}.profiles.<profile>.extensions.settings`](#opt-${moduleName}.profiles._name_.extensions.settings).
Persisting only the `extensions` directory is generally
not sufficient, because ${name} stores extension state
in other profile files and databases that are managed
outside Home Manager.
'';
};
force = mkOption {
description = ''
Whether to override all previous firefox settings.
This is required when using `settings`.
'';
default = false;
example = true;
type = types.bool;
};
exhaustivePermissions = mkOption {
description = ''
When enabled, the user must authorize requested
permissions for all extensions from
{option}`${moduleName}.profiles.<profile>.extensions.packages`
in
{option}`${moduleName}.profiles.<profile>.extensions.settings.<extensionID>.permissions`
'';
default = false;
example = true;
type = types.bool;
};
exactPermissions = mkOption {
description = ''
When enabled,
{option}`${moduleName}.profiles.<profile>.extensions.settings.<extensionID>.permissions`
must specify the exact set of permissions that the
extension will request.
This means that if the authorized permissions are
broader than what the extension requests, the
assertion will fail.
'';
default = false;
example = true;
type = types.bool;
};
settings = mkOption {
default = { };
example = literalExpression ''
{
# Example with uBlock origin's extensionID
"uBlock0@raymondhill.net".settings = {
selectedFilterLists = [
"ublock-filters"
"ublock-badware"
"ublock-privacy"
"ublock-unbreak"
"ublock-quick-fixes"
];
};
# Example with Stylus' UUID-form extensionID
"{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}".settings = {
dbInChromeStorage = true; # required for Stylus
}
}
'';
description = ''
Attribute set of options for each extension.
The keys of the attribute set consist of the ID of the extension
or its UUID wrapped in curly braces.
'';
type = types.attrsOf (
types.submodule {
options = {
settings = mkOption {
type = types.attrsOf jsonFormat.type;
default = { };
description = "Json formatted options for this extension.";
};
permissions = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [ "activeTab" ];
defaultText = "Any permissions";
description = ''
Allowed permissions for this extension. See
<https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions>
for a list of relevant permissions.
'';
};
force = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
Forcibly override any existing configuration for
this extension.
'';
};
};
}
);
};
};
};
default = { };
description = ''
Submodule for installing and configuring extensions.
'';
example = literalExpression ''
{
packages = with pkgs.nur.repos.rycee.firefox-addons; [
ublock-origin
];
settings."uBlock0@raymondhill.net".settings = {
selectedFilterLists = [
"ublock-filters"
"ublock-badware"
"ublock-privacy"
"ublock-unbreak"
"ublock-quick-fixes"
];
};
}
'';
};
};
config = {
settings = mkMerge [
(mkIf (config.userChrome != "") {
"toolkit.legacyUserProfileCustomizations.stylesheets" = mkDefault true;
})
(mkIf (config.storeId != null) {
"toolkit.profiles.storeID" = mkDefault config.storeId;
})
];
assertions = [
(mkNoDuplicateAssertion config.containers "container")
{
assertion = !(extensionSettingsMissingForce config.extensions.settings) || config.extensions.force;
message = ''
Using '${lib.showOption profilePath}.extensions.settings' will override all
previous extensions settings. Enable either
'${lib.showOption profilePath}.extensions.force' or the corresponding
'${lib.showOption profilePath}.extensions.settings.<extensionId>.force'
to acknowledge this.
'';
}
{
assertion =
(config.storeId == null)
|| ((config.settings."toolkit.profiles.storeID" or config.storeId) == config.storeId);
message = ''
${moduleName}.profiles.${name}.storeId must match
${moduleName}.profiles.${name}.settings."toolkit.profiles.storeID"
'';
}
]
++ (builtins.concatMap (
{
addonId ? null,
name,
meta,
...
}:
let
safeAddonId = if addonId != null then addonId else name;
permissions = config.extensions.settings.${safeAddonId}.permissions or null;
requireCheck = config.extensions.exhaustivePermissions || permissions != null;
authorizedPermissions = lib.optionals (permissions != null) permissions;
missingPermissions = lib.subtractLists authorizedPermissions meta.mozPermissions;
redundantPermissions = lib.subtractLists meta.mozPermissions authorizedPermissions;
checkSatisfied =
if config.extensions.exactPermissions then
missingPermissions == [ ] && redundantPermissions == [ ]
else
missingPermissions == [ ];
errorMessage =
if
config.extensions.exactPermissions && missingPermissions != [ ] && redundantPermissions != [ ]
then
''
Extension ${safeAddonId} requests permissions that weren't
authorized: ${builtins.toJSON missingPermissions}.
Additionally, the following permissions were authorized,
but extension ${safeAddonId} did not request them:
${builtins.toJSON redundantPermissions}.
Consider adjusting the permissions in''
else if config.extensions.exactPermissions && redundantPermissions != [ ] then
''
The following permissions were authorized, but extension
${safeAddonId} did not request them: ${builtins.toJSON redundantPermissions}.
Consider removing the redundant permissions from''
else
''
Extension ${safeAddonId} requests permissions that weren't
authorized: ${builtins.toJSON missingPermissions}.
Consider adding the missing permissions to'';
in
[
{
assertion = !requireCheck || checkSatisfied;
message = ''
${errorMessage}
'${
lib.showAttrPath (
profilePath
++ [
"extensions"
safeAddonId
]
)
}.permissions'.
'';
}
]
) config.extensions.packages)
++ (builtins.concatMap (
{
name,
value,
}:
let
packages = builtins.filter (pkg: (pkg.addonId or pkg.name) == name) config.extensions.packages;
in
[
{
assertion = value.permissions == null || length packages == 1;
message = ''
Must have exactly one extension with addonId '${name}'
in '${lib.showOption profilePath}.extensions.packages' but found ${toString (length packages)}.
'';
}
]
) (lib.attrsToList config.extensions.settings))
++ config.bookmarks.assertions;
};
}
)
);
default = { };
example = lib.optionalAttrs (moduleName == "programs.firefox") (literalExpression ''
{
"dev-edition-default" = {
id = 0;
path = config.home.username;
settings = {
"browser.aboutConfig.showWarning" = false;
};
};
}
'');
description = ''
Attribute set of ${appName} profiles.
${lib.optionalString (moduleName == "programs.firefox") ''
When using Firefox Developer Edition, the profile name should be
`dev-edition-default`. You can still set {option}`path` to store the
profile in a custom directory.
''}
'';
};
enableGnomeExtensions = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable the GNOME Shell native host connector. Note, you
also need to set the NixOS option
`services.gnome.gnome-browser-connector.enable` to
`true`.
'';
};
pkcs11Modules = mkOption {
type = types.listOf types.package;
default = [ ];
description = ''
Additional packages to be loaded as PKCS #11 modules in Firefox.
'';
};
};
config = mkIf cfg.enable (
{
assertions = [
(lib.hm.assertions.assertPlatform moduleName pkgs supportedPlatforms)
(
let
defaults = lib.catAttrs "name" (lib.filter (a: a.isDefault) (attrValues cfg.profiles));
in
{
assertion = cfg.profiles == { } || length defaults == 1;
message =
"Must have exactly one default ${appName} profile but found "
+ toString (length defaults)
+ optionalString (length defaults > 1) (", namely " + concatStringsSep ", " defaults);
}
)
(
let
getContainers =
profiles: lib.flatten (mapAttrsToList (_: value: (attrValues value.containers)) profiles);
findInvalidContainerIds =
profiles:
lib.filter (container: container.id == 0 || container.id >= 4294967294) (getContainers profiles);
in
{
assertion = cfg.profiles == { } || length (findInvalidContainerIds cfg.profiles) == 0;
message = "Container id must be between 1 and 4294967293";
}
)
{
assertion = cfg.languagePacks == [ ] || cfg.package != null;
message = ''
'${moduleName}.languagePacks' requires '${moduleName}.package'
to be set to a non-null value.
'';
}
{
assertion =
cfg.globalExtensions == [ ] || cfg.package != null || (isDarwin && cfg.darwinDefaultsId != null);
message =
"'${moduleName}.globalExtensions' requires '${moduleName}.package'"
+ " to be set to a non-null value unless"
+ " '${moduleName}.darwinDefaultsId' is set on Darwin.";
}
{
assertion = builtins.all (
elem:
let
package = elem.package or elem;
in
package ? addonId
) cfg.globalExtensions;
message = "${moduleName}.globalExtensions requires each package to expose addonId in passthru.";
}
(mkNoDuplicateAssertion cfg.profiles "profile")
(
let
profilesWithStoreId = lib.filterAttrs (_: profile: profile.storeId != null) cfg.profiles;
duplicateStoreIds = lib.filterAttrs (_storeId: profileNames: length profileNames != 1) (
lib.zipAttrs (
mapAttrsToList (profileName: profile: {
"${profile.storeId}" = profileName;
}) profilesWithStoreId
)
);
mkMsg =
storeId: profileNames: " - StoreID ${storeId} is used by " + (concatStringsSep ", " profileNames);
in
{
assertion = cfg.profiles == { } || duplicateStoreIds == { };
message = ''
Must not have duplicate ${appName} profile StoreIDs:
${concatStringsSep "\n" (mapAttrsToList mkMsg duplicateStoreIds)}
'';
}
)
]
++ (lib.concatMap (profile: profile.assertions) (attrValues cfg.profiles));
warnings =
optional (cfg.enableGnomeExtensions or false) ''
Using '${moduleName}.enableGnomeExtensions' has been deprecated and
will be removed in the future. Please change to overriding the package
configuration using '${moduleName}.package' instead. You can refer to
its example for how to do this.
''
++ optional (cfg.vendorPath != null) ''
Using '${moduleName}.vendorPath' has been deprecated and
will be removed in the future. Native messaging hosts will function normally without specifying this path.
''
++ lib.flatten (
lib.mapAttrsToList (
name: profile:
lib.optional (profile.bookmarks._legacySettings != null) (
let
legacySettingsExample =
if profile.bookmarks._legacySettings == "a list" then "[ ... ]" else "{ ... }";
in
lib.hm.deprecations.mkDeprecatedOptionValueWarning {
option = modulePath ++ [
"profiles"
name
"bookmarks"
];
old = profile.bookmarks._legacySettings;
replacement = "`${
lib.showOption (
modulePath
++ [
"profiles"
name
"bookmarks"
"settings"
]
)
}` with `${
lib.showOption (
modulePath
++ [
"profiles"
name
"bookmarks"
"force"
]
)
} = true`";
details = ''
Set `force = true` to acknowledge replacing existing custom bookmarks.
Replace:
${moduleName}.profiles.${name}.bookmarks = ${legacySettingsExample};
With:
${moduleName}.profiles.${name}.bookmarks = {
force = true;
settings = ${legacySettingsExample};
};
'';
}
)
) cfg.profiles
);
targets.darwin.defaults = (
mkIf (cfg.darwinDefaultsId != null && isDarwin) {
${cfg.darwinDefaultsId} = {
EnterprisePoliciesEnabled = true;
}
// cfg.policies;
}
);
home.packages = lib.optional (cfg.finalPackage != null) cfg.finalPackage;
home.file = mkMerge (
[
{
"${cfg.configPath}/profiles.ini" = mkIf (cfg.profiles != { }) { text = profilesIni; };
}
]
++ lib.flip mapAttrsToList cfg.profiles (
_: profile:
# Merge the regular profile settings with extension settings
mkMerge [
{
"${cfg.profilesPath}/${profile.path}/.keep".text = "";
"${cfg.profilesPath}/${profile.path}/chrome/userChrome.css" = mkIf (profile.userChrome != "") (
let
key = if builtins.isString profile.userChrome then "text" else "source";
in
{
"${key}" = profile.userChrome;
}
);
"${cfg.profilesPath}/${profile.path}/chrome/userContent.css" = mkIf (profile.userContent != "") (
let
key = if builtins.isString profile.userContent then "text" else "source";
in
{
"${key}" = profile.userContent;
}
);
"${cfg.profilesPath}/${profile.path}/user.js" =
mkIf
(
profile.preConfig != ""
|| profile.settings != { }
|| profile.extraConfig != ""
|| profile.bookmarks.configFile != null
|| extensionSettingsNeedForce profile.extensions.settings
)
{
text =
mkUserJs profile.preConfig profile.settings profile.extraConfig profile.bookmarks.configFile
profile.extensions.settings;
};
"${cfg.profilesPath}/${profile.path}/containers.json" = mkIf (profile.containers != { }) {
text = mkContainersJson profile.containers;
force = profile.containersForce;
};
"${cfg.profilesPath}/${profile.path}/search.json.mozlz4" = mkIf (profile.search.enable) {
inherit (profile.search) enable force;
source = profile.search.file;
};
"${cfg.profilesPath}/${profile.path}/handlers.json" = mkIf (profile.handlers.enable) {
source = profile.handlers.configFile;
inherit (profile.handlers) force;
};
"${cfg.profilesPath}/${profile.path}/extensions" = mkIf (profile.extensions.packages != [ ]) {
source =
let
extensionsEnvPkg = pkgs.buildEnv {
name = "hm-firefox-extensions";
paths = profile.extensions.packages;
};
in
"${extensionsEnvPkg}/share/mozilla/${extensionPath}";
recursive = true;
force = true;
};
}
(mkMerge (
mapAttrsToList (
name: settingConfig:
mkIf (settingConfig.settings != { }) {
"${cfg.profilesPath}/${profile.path}/browser-extension-data/${name}/storage.js" = {
force = settingConfig.force || profile.extensions.force;
text = lib.generators.toJSON { } settingConfig.settings;
};
}
) profile.extensions.settings
))
]
)
);
}
// setAttrByPath modulePath {
finalPackage = wrapPackage cfg.package;
release = mkOptionDefault (builtins.head (lib.splitString "-" cfg.package.version));
policies = {
NoDefaultBookmarks = lib.mkIf (builtins.any (profile: profile.bookmarks.enable) (
builtins.attrValues cfg.profiles
)) false;
ExtensionSettings = mkMerge [
(lib.mkIf (cfg.languagePacks != [ ]) (
lib.listToAttrs (
map (
lang:
lib.nameValuePair "langpack-${lang}@firefox.mozilla.org" {
installation_mode = "normal_installed";
install_url = "https://releases.mozilla.org/pub/firefox/releases/${cfg.release}/linux-x86_64/xpi/${lang}.xpi";
}
) cfg.languagePacks
)
))
(lib.mkIf (cfg.globalExtensions != [ ]) (
lib.listToAttrs (
map (
elem:
let
package = elem.package or elem;
settings = elem.settings or { };
in
lib.nameValuePair package.addonId (
{
installation_mode = "force_installed";
install_url = "file://${package.outPath}/share/mozilla/${extensionPath}/${package.addonId}.xpi";
}
// settings
)
) (builtins.filter (elem: (elem.package or elem) ? addonId) cfg.globalExtensions)
)
))
];
};
}
);
}