diff --git a/modules/misc/news/2026/04/2026-04-23_18-35-00.nix b/modules/misc/news/2026/04/2026-04-23_18-35-00.nix new file mode 100644 index 000000000..f337a0514 --- /dev/null +++ b/modules/misc/news/2026/04/2026-04-23_18-35-00.nix @@ -0,0 +1,13 @@ +{ config, ... }: +{ + time = "2026-04-23T18:35:00+00:00"; + condition = config.programs.firefox.enable; + message = '' + The deprecated `programs.firefox.profiles..extensions = [ ... ]` + shorthand has been removed. + + Use `programs.firefox.profiles..extensions.packages = [ ... ]` + to install add-ons, and keep declarative extension settings under + `programs.firefox.profiles..extensions.settings`. + ''; +} diff --git a/modules/programs/firefox/mkFirefoxModule.nix b/modules/programs/firefox/mkFirefoxModule.nix index 00d3718ec..4974005b4 100644 --- a/modules/programs/firefox/mkFirefoxModule.nix +++ b/modules/programs/firefox/mkFirefoxModule.nix @@ -661,152 +661,137 @@ in ''; }; 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..extensions - to - ${moduleName}.profiles..extensions.packages - '' packages + 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 '' -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..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..extensions.packages` + in + {option}`${moduleName}.profiles..extensions.settings..permissions` + ''; + default = false; + example = true; + type = types.bool; + }; + + exactPermissions = mkOption { + description = '' + When enabled, + {option}`${moduleName}.profiles..extensions.settings..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 + + for a list of relevant permissions. + ''; + }; + force = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Forcibly override any existing configuration for + this extension. + ''; + }; + }; + } ); - }) - ( - 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 '' -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..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..extensions.packages` - in - {option}`${moduleName}.profiles..extensions.settings..permissions` - ''; - default = false; - example = true; - type = types.bool; - }; - - exactPermissions = mkOption { - description = '' - When enabled, - {option}`${moduleName}.profiles..extensions.settings..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 - - 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. diff --git a/tests/modules/programs/firefox/common.nix b/tests/modules/programs/firefox/common.nix index c169d1508..a9c04d57b 100644 --- a/tests/modules/programs/firefox/common.nix +++ b/tests/modules/programs/firefox/common.nix @@ -21,6 +21,7 @@ builtins.mapAttrs "${name}-profiles-duplicate-ids" = ./profiles/duplicate-ids.nix; "${name}-profiles-extensions" = ./profiles/extensions; "${name}-profiles-extensions-assertions" = ./profiles/extensions/assertions.nix; + "${name}-profiles-extensions-extensible" = ./profiles/extensions/extensible.nix; "${name}-profiles-extensions-exhaustive" = ./profiles/extensions/exhaustive.nix; "${name}-profiles-extensions-exact" = ./profiles/extensions/exact.nix; "${name}-profiles-handlers" = ./profiles/handlers; diff --git a/tests/modules/programs/firefox/profiles/extensions/extensible.nix b/tests/modules/programs/firefox/profiles/extensions/extensible.nix new file mode 100644 index 000000000..c218f24f4 --- /dev/null +++ b/tests/modules/programs/firefox/profiles/extensions/extensible.nix @@ -0,0 +1,67 @@ +modulePath: +{ lib, pkgs, ... }: + +let + homeManagerModules = import ../../../../../../modules/modules.nix { + inherit lib pkgs; + check = false; + }; + extensionsOptionPath = modulePath ++ [ + "profiles" + "default" + "extensions" + ]; + thirdPartyOptionPath = extensionsOptionPath ++ [ "thirdParty" ]; + packagesOptionPath = extensionsOptionPath ++ [ "packages" ]; + + evalResult = builtins.tryEval ( + let + evaluated = lib.evalModules { + specialArgs = { inherit pkgs; }; + + modules = homeManagerModules ++ [ + { + home = { + username = "hm-user"; + homeDirectory = "/home/hm-user"; + stateVersion = "25.05"; + }; + } + { + options = lib.setAttrByPath (modulePath ++ [ "profiles" ]) ( + lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options.extensions.thirdParty = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Third-party regression test option."; + }; + } + ); + } + ); + + config = lib.setAttrByPath thirdPartyOptionPath true; + } + { + config = lib.setAttrByPath packagesOptionPath [ pkgs.hello ]; + } + ]; + }; + in + { + thirdParty = lib.getAttrFromPath thirdPartyOptionPath evaluated.config; + packageCount = builtins.length (lib.getAttrFromPath packagesOptionPath evaluated.config); + } + ); +in +{ + nmt.script = '' + test '${builtins.toJSON evalResult.success}' = 'true' + test '${ + builtins.toJSON (if evalResult.success then evalResult.value.thirdParty else null) + }' = 'true' + test '${builtins.toJSON (if evalResult.success then evalResult.value.packageCount else null)}' = '1' + ''; +}