mirror of
https://github.com/nix-community/home-manager.git
synced 2025-11-08 19:46:05 +01:00
Properly handle extensions missing addonId without eval error. Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
1110 lines
40 KiB
Nix
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
|
|
)
|
|
);
|
|
};
|
|
}
|
|
);
|
|
}
|