1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 19:46:05 +01:00
home-manager/modules/programs/firefox/mkFirefoxModule.nix
Austin Horstman 5200f3903f mkFirefoxModule: support extensions without addonID
Properly handle extensions missing addonId without eval error.

Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-10-10 08:40:00 -05:00

1110 lines
40 KiB
Nix

{
modulePath,
name,
description ? null,
wrappedPackageName ? null,
unwrappedPackageName ? null,
platforms,
visible ? false,
enableBookmarks ? true,
}:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
attrValues
concatStringsSep
length
literalExpression
mapAttrsToList
mkIf
mkMerge
mkOption
mkOptionDefault
optionalString
optional
setAttrByPath
types
;
inherit (pkgs.stdenv.hostPlatform) isDarwin;
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;
}
)
// {
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 then pref else builtins.toJSON pref
);
extensionSettingsNeedForce =
extensionSettings: builtins.any (ext: ext.settings != { }) (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;
name = container.name;
icon = container.icon;
color = container.color;
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 = {
enableGnomeExtensions = 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;
in
if package == null then
null
else if isWrapped then
package.override (old: {
cfg = old.cfg or { } // fcfg;
extraPolicies = (old.extraPolicies or { }) // cfg.policies;
pkcs11Modules = (old.pkcs11Modules or [ ]) ++ cfg.pkcs11Modules;
})
else
(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}"}
${optionalString (!visible) "See `${moduleName}` for more configuration options."}
'';
};
package = mkOption {
inherit visible;
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`.";
};
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 =
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 {
internal = true;
type = types.str;
default = with platforms; if isDarwin then darwin.configPath else linux.configPath;
example = ".mozilla/firefox";
description = "Directory containing the ${appName} configuration files.";
};
nativeMessagingHosts = mkOption {
inherit visible;
type = types.listOf types.package;
default = [ ];
description = ''
Additional packages containing native messaging hosts that should be
made available to ${appName} extensions.
'';
};
finalPackage = mkOption {
inherit visible;
type = with types; nullOr package;
readOnly = true;
description = "Resulting ${appName} package.";
};
policies = lib.optionalAttrs (wrappedPackageName != null) (mkOption {
inherit visible;
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 {
inherit visible;
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.
'';
};
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
lib.warn
''
${cfg.name} bookmarks have been refactored into a submodule that now explicitly require a 'force' option to be enabled.
Replace:
${moduleName}.profiles.${name}.bookmarks = [ ... ];
With:
${moduleName}.profiles.${name}.bookmarks = {
force = true;
settings = [ ... ];
};
''
{
force = true;
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.";
};
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 = 0;
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.coercedTo (types.listOf types.package)
(packages: {
packages = mkIf (builtins.length packages > 0) (
lib.warn ''
In order to support declarative extension configuration,
extension installation has been moved from
${moduleName}.profiles.<profile>.extensions
to
${moduleName}.profiles.<profile>.extensions.packages
'' packages
);
})
(
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)
'';
};
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 = {
assertions = [
(mkNoDuplicateAssertion config.containers "container")
{
assertion = !(extensionSettingsNeedForce config.extensions.settings) || config.extensions.force;
message = ''
Using '${lib.showOption profilePath}.extensions.settings' will override all
previous extensions settings. Enable
'${lib.showOption profilePath}.extensions.force' to acknowledge this.
'';
}
]
++ (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 = { };
description = "Attribute set of ${appName} profiles.";
};
enableGnomeExtensions = mkOption {
inherit visible;
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 >= 4294967294) (getContainers profiles);
in
{
assertion = cfg.profiles == { } || length (findInvalidContainerIds cfg.profiles) == 0;
message = "Container id must be smaller than 4294967294 (2^32 - 2)";
}
)
{
assertion = cfg.languagePacks == [ ] || cfg.package != null;
message = ''
'${moduleName}.languagePacks' requires '${moduleName}.package'
to be set to a non-null value.
'';
}
(mkNoDuplicateAssertion cfg.profiles "profile")
]
++ (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.
'';
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) {
enable = profile.search.enable;
force = profile.search.force;
source = profile.search.file;
};
"${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 = 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
)
);
};
}
);
}