Unverified Commit 72ed3377 authored by Hans Christian Schmitz's avatar Hans Christian Schmitz
Browse files

nixos/wireplumber: add `extraConfig`/`extraScripts` options

Follow-up to #282377. #282377 broke `environment.etc."wireplumber<...>"`,
however WirePlumber did not yet have `extraConfig` style options for
configuring it ergonomically outside of `environment.etc`. This has
caused issues for people who had custom config files for WirePlumber, as
having to create a config package just to edit some settings is not as
ergonomic or discoverable as with a proper `extraConfig` style option.

This commit fixes this issue by adding the `extraConfig` option for
additional config file and the `extraScripts` option for additional
scripts to be used by config files.

With WirePlumber 0.5 it is possible to supply config files and scripts
via the `XDG_DATA_DIRS` variable to the WirePlumber daemon. This is how
the new options and with this change also the `configPackages` option
expose their files to the daemon. This way
`environment.etc."wireplumber"` works again for user configuration and
breakage of old configs from 23.11 to 24.05 should be limited to those
caused by the change in the config format from WirePlumber 0.4 to 0.5.
parent b2245dab
Loading
Loading
Loading
Loading
+159 −24
Original line number Diff line number Diff line
{ config, lib, pkgs, ... }:

let
  inherit (builtins) attrNames concatMap length;
  inherit (builtins) concatMap;
  inherit (lib) maintainers;
  inherit (lib.attrsets) attrByPath filterAttrs;
  inherit (lib.attrsets) attrByPath mapAttrsToList;
  inherit (lib.lists) flatten optional;
  inherit (lib.modules) mkIf;
  inherit (lib.options) literalExpression mkOption;
  inherit (lib.strings) hasPrefix;
  inherit (lib.types) bool listOf package;
  inherit (lib.strings) concatStringsSep makeSearchPath;
  inherit (lib.types) bool listOf attrsOf package lines;
  inherit (lib.path) subpath;

  pwCfg = config.services.pipewire;
  cfg = pwCfg.wireplumber;
  pwUsedForAudio = pwCfg.audio.enable;

  json = pkgs.formats.json { };

  configSectionsToConfFile = path: value:
    pkgs.writeTextDir
      path
      (concatStringsSep "\n" (
        mapAttrsToList
          (section: content: "${section} = " + (builtins.toJSON content))
          value
      ));

  mapConfigToFiles = config:
    mapAttrsToList
      (name: value: configSectionsToConfFile "share/wireplumber/wireplumber.conf.d/${name}.conf" value)
      config;

  mapScriptsToFiles = scripts:
    mapAttrsToList
      (relativePath: value: pkgs.writeTextDir (subpath.join ["share/wireplumber/scripts" relativePath]) value)
      scripts;
in
{
  meta.maintainers = [ maintainers.k900 ];
@@ -33,6 +55,114 @@ in
        description = "The WirePlumber derivation to use.";
      };

      extraConfig = mkOption {
        # Two layer attrset is necessary before using JSON, because of the whole
        # config file not being a JSON object, but a concatenation of JSON objects
        # in sections.
        type = attrsOf (attrsOf json.type);
        default = { };
        example = literalExpression ''{
          "log-level-debug" = {
            "context.properties" = {
              # Output Debug log messages as opposed to only the default level (Notice)
              "log.level" = "D";
            };
          };
          "wh-1000xm3-ldac-hq" = {
            "monitor.bluez.rules" = [
              {
                matches = [
                  {
                    # Match any bluetooth device with ids equal to that of a WH-1000XM3
                    "device.name" = "~bluez_card.*";
                    "device.product.id" = "0x0cd3";
                    "device.vendor.id" = "usb:054c";
                  }
                ];
                actions = {
                  update-props = {
                    # Set quality to high quality instead of the default of auto
                    "bluez5.a2dp.ldac.quality" = "hq";
                  };
                };
              }
            ];
          };
        }'';
        description = ''
          Additional configuration for the WirePlumber daemon when run in
          single-instance mode (the default in nixpkgs and currently the only
          supported way to run WirePlumber configured via `extraConfig`).

          See also:
          - [The configuration file][docs-the-conf-file]
          - [Modifying configuration][docs-modifying-config]
          - [Locations of files][docs-file-locations]
          - and the [configuration section][docs-config-section] of the docs in general

          Note that WirePlumber (and PipeWire) use dotted attribute names like
          `device.product.id`. These are not nested, but flat objects for WirePlumber/PipeWire,
          so to write these in nix expressions, remember to quote them like `"device.product.id"`.
          Have a look at the example for this.

          [docs-the-conf-file]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/conf_file.html
          [docs-modifying-config]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/modifying_configuration.html
          [docs-file-locations]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/locations.html
          [docs-config-section]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration.html
        '';
      };

      extraScripts = mkOption {
        type = attrsOf lines;
        default = { };
        example = {
          "test/hello-world.lua" = ''
            print("Hello, world!")
          '';
        };
        description = ''
          Additional scripts for WirePlumber to be used by configuration files.

          Every item in this attrset becomes a separate lua file with the path
          relative to the `scripts` directory specified in the name of the item.
          The scripts get passed to the WirePlumber service via the `XDG_DATA_DIRS`
          variable. Scripts specified here are preferred over those shipped with
          WirePlumber if they occupy the same relative path.

          For a script to be loaded, it needs to be specified as part of a component,
          and that component needs to be required by an active profile (e.g. `main`).
          Components can be defined in config files either via `extraConfig` or `configPackages`.

          For the hello-world example, you'd have to add the following `extraConfig`:
          ```nix
            services.pipewire.wireplumber.extraConfig."99-hello-world" = {
              "wireplumber.components" = [
                {
                  name = "test/hello-world.lua";
                  type = "script/lua";
                  provides = "custom.hello-world";
                }
              ];

              "wireplumber.profiles" = {
                main = {
                  "custom.hello-world" = "required";
                };
              };
            };
          ```

          See also:
          - [Location of scripts][docs-file-locations-scripts]
          - [Components & Profiles][docs-components-profiles]
          - [Migration - Loading custom scripts][docs-migration-loading-custom-scripts]

          [docs-file-locations-scripts]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/locations.html#location-of-scripts
          [docs-components-profiles]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/components_and_profiles.html
          [docs-migration-loading-custom-scripts]: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/migration.html#loading-custom-scripts
        '';
      };

      configPackages = mkOption {
        type = listOf package;
        default = [ ];
@@ -96,7 +226,20 @@ in
        }
      '';

      extraConfigPkg = pkgs.buildEnv {
        name = "wireplumber-extra-config";
        paths = mapConfigToFiles cfg.extraConfig;
        pathsToLink = [ "/share/wireplumber/wireplumber.conf.d" ];
      };

      extraScriptsPkg = pkgs.buildEnv {
        name = "wireplumber-extra-scrips";
        paths = mapScriptsToFiles cfg.extraScripts;
        pathsToLink = [ "/share/wireplumber/scripts" ];
      };

      configPackages = cfg.configPackages
        ++ [ extraConfigPkg extraScriptsPkg ]
        ++ optional (!pwUsedForAudio) pwNotForAudioConfigPkg
        ++ optional pwCfg.systemWide systemwideConfigPkg;

@@ -127,24 +270,10 @@ in
          assertion = !config.hardware.bluetooth.hsphfpd.enable;
          message = "Using WirePlumber conflicts with hsphfpd, as it provides the same functionality. `hardware.bluetooth.hsphfpd.enable` needs be set to false";
        }
        {
          assertion = length
            (attrNames
              (
                filterAttrs
                  (name: value:
                    hasPrefix "wireplumber/" name || name == "wireplumber"
                  )
                  config.environment.etc
              )) == 1;
          message = "Using `environment.etc.\"wireplumber<...>\"` directly is no longer supported in 24.05. Use `services.pipewire.wireplumber.configPackages` instead.";
        }
      ];

      environment.systemPackages = [ cfg.package ];

      environment.etc.wireplumber.source = "${configs}/share/wireplumber";

      systemd.packages = [ cfg.package ];

      systemd.services.wireplumber.enable = pwCfg.systemWide;
@@ -156,10 +285,16 @@ in
      systemd.services.wireplumber.environment = mkIf pwCfg.systemWide {
        # Force WirePlumber to use system dbus.
        DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/dbus/system_bus_socket";

        # Make WirePlumber find our config/script files and lv2 plugins required by those
        # (but also the configs/scripts shipped with WirePlumber)
        XDG_DATA_DIRS = makeSearchPath "share" [ configs cfg.package ];
        LV2_PATH = "${lv2Plugins}/lib/lv2";
      };

      systemd.user.services.wireplumber.environment.LV2_PATH =
        mkIf (!pwCfg.systemWide) "${lv2Plugins}/lib/lv2";
      systemd.user.services.wireplumber.environment = mkIf (!pwCfg.systemWide) {
        XDG_DATA_DIRS = makeSearchPath "share" [ configs cfg.package ];
        LV2_PATH = "${lv2Plugins}/lib/lv2";
      };
    };
}