nixos/mpd: apply RFC 42 (#456989)

This commit is contained in:
Doron Behar
2025-12-15 14:26:09 +00:00
committed by GitHub
5 changed files with 310 additions and 166 deletions
+258 -114
View File
@@ -12,46 +12,85 @@ let
gid = config.ids.gids.mpd;
cfg = config.services.mpd;
credentialsPlaceholder = (
creds:
let
placeholders = (
lib.imap0 (
i: c: ''password "{{password-${toString i}}}@${lib.concatStringsSep "," c.permissions}"''
) creds
);
in
lib.concatStringsSep "\n" placeholders
);
mpdConf = pkgs.writeText "mpd.conf" ''
# This file was automatically generated by NixOS. Edit mpd's configuration
# via NixOS' configuration.nix, as this file will be rewritten upon mpd's
# restart.
music_directory "${cfg.musicDirectory}"
playlist_directory "${cfg.playlistDirectory}"
${lib.optionalString (cfg.dbFile != null) ''
db_file "${cfg.dbFile}"
''}
state_file "${cfg.dataDir}/state"
sticker_file "${cfg.dataDir}/sticker.sql"
${lib.optionalString (
cfg.network.listenAddress != "any"
) ''bind_to_address "${cfg.network.listenAddress}"''}
${lib.optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''}
${lib.optionalString (cfg.fluidsynth) ''
decoder {
plugin "fluidsynth"
soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2"
}
''}
${lib.optionalString (cfg.credentials != [ ]) (credentialsPlaceholder cfg.credentials)}
${cfg.extraConfig}
'';
mkKeyValue =
a:
lib.mapAttrsToList (
k: v:
k
+ " "
+ (
if builtins.isBool v then
# Mainly for https://mpd.readthedocs.io/en/stable/user.html#zeroconf
"\"" + (lib.boolToYesNo v) + "\""
else
"\"" + (toString v) + "\""
)
) a;
nonBlockSettings = lib.filterAttrs (n: v: !(builtins.isAttrs v || builtins.isList v)) cfg.settings;
pureBlockSettings = builtins.removeAttrs cfg.settings (builtins.attrNames nonBlockSettings);
blocks =
pureBlockSettings
// lib.optionalAttrs cfg.fluidsynth {
decoder = (pureBlockSettings.decoder or [ ]) ++ [
{
plugin = "fluidsynth";
soundfont = "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2";
}
];
};
processSingleBlock =
n: v:
[
(n + " {")
]
# Add indentation, for better readability
++ (map (l: " " + l) (mkKeyValue v))
++ [ "}" ];
mpdConf = pkgs.writeTextFile {
name = "mpd.conf";
text = ''
# This file was automatically generated by NixOS. Edit mpd's configuration
# via NixOS' configuration.nix, as this file will be rewritten upon mpd's
# restart.
''
+ lib.concatStringsSep "\n" (
mkKeyValue (
{
state_file = "${cfg.dataDir}/state";
sticker_file = "${cfg.dataDir}/sticker.sql";
}
// nonBlockSettings
)
++ lib.flatten (
lib.mapAttrsToList (
n: v: if builtins.isList v then (map (b: processSingleBlock n b) v) else (processSingleBlock n v)
) blocks
)
++ lib.imap0 (
i: a: "password \"{{password-${toString i}}}@${lib.concatStringsSep "," a.permissions}\""
) cfg.credentials
);
derivationArgs = {
expectScript = ''
spawn ${lib.getExe pkgs.buildPackages.mpd} --no-daemon "$env(out)"
expect {
"exception: Error in \"$env(out)\"" {
puts "Config file invalid\n"
exit 1
}
"exception:" {
exit 0
}
}
'';
passAsFile = [
"expectScript"
];
};
checkPhase = ''
${lib.getExe pkgs.buildPackages.expect} -f "$expectScriptPath"
'';
};
in
{
@@ -80,39 +119,16 @@ in
'';
};
musicDirectory = lib.mkOption {
type = with lib.types; either path (strMatching "(http|https|nfs|smb)://.+");
default = "${cfg.dataDir}/music";
defaultText = lib.literalExpression ''"''${dataDir}/music"'';
description = ''
The directory or NFS/SMB network share where MPD reads music from. If left
as the default value this directory will automatically be created before
the MPD server starts, otherwise the sysadmin is responsible for ensuring
the directory exists with appropriate ownership and permissions.
'';
user = lib.mkOption {
type = lib.types.str;
default = name;
description = "User account under which MPD runs.";
};
playlistDirectory = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/playlists";
defaultText = lib.literalExpression ''"''${dataDir}/playlists"'';
description = ''
The directory where MPD stores playlists. If left as the default value
this directory will automatically be created before the MPD server starts,
otherwise the sysadmin is responsible for ensuring the directory exists
with appropriate ownership and permissions.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra directives added to to the end of MPD's configuration file,
mpd.conf. Basic configuration like file location and uid/gid
is added automatically to the beginning of the file. For available
options see {manpage}`mpd.conf(5)`.
'';
group = lib.mkOption {
type = lib.types.str;
default = name;
description = "Group account under which MPD runs.";
};
dataDir = lib.mkOption {
@@ -126,48 +142,135 @@ in
'';
};
user = lib.mkOption {
type = lib.types.str;
default = name;
description = "User account under which MPD runs.";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open ports in the firewall for mpd.";
};
group = lib.mkOption {
type = lib.types.str;
default = name;
description = "Group account under which MPD runs.";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType =
let
inherit (lib.types)
oneOf
attrsOf
listOf
str
int
bool
path
;
atomType = oneOf [
str
int
bool
path
];
in
attrsOf (oneOf [
atomType
(listOf (attrsOf atomType))
]);
options = {
music_directory = lib.mkOption {
type = with lib.types; either path (strMatching "([a-z]+)://.+");
default = "${cfg.dataDir}/music";
defaultText = lib.literalExpression ''"''${dataDir}/music"'';
description = ''
The directory or URI where MPD reads music from. If left
as the default value this directory will automatically be created before
the MPD server starts, otherwise the sysadmin is responsible for ensuring
the directory exists with appropriate ownership and permissions.
'';
};
network = {
playlist_directory = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/playlists";
defaultText = lib.literalExpression ''"''${dataDir}/playlists"'';
description = ''
The directory where MPD stores playlists. If left as the default value
this directory will automatically be created before the MPD server starts,
otherwise the sysadmin is responsible for ensuring the directory exists
with appropriate ownership and permissions.
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "any";
description = ''
The address for the daemon to listen on.
Use `any` to listen on all addresses.
'';
bind_to_address = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
example = "any";
description = ''
The address for the daemon to listen on.
Use `any` to listen on all addresses.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 6600;
description = ''
This setting is the TCP port that is desired for the daemon to get assigned
to.
'';
};
db_file = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/tag_cache";
defaultText = lib.literalExpression ''"''${dataDir}/tag_cache"'';
description = ''
The path to MPD's database.
'';
};
};
};
port = lib.mkOption {
type = lib.types.port;
default = 6600;
description = ''
This setting is the TCP port that is desired for the daemon to get assigned
to.
'';
};
};
dbFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "${cfg.dataDir}/tag_cache";
defaultText = lib.literalExpression ''"''${dataDir}/tag_cache"'';
default = { };
description = ''
The path to MPD's database. If set to `null` the
parameter is omitted from the configuration.
Configuration for MPD. MPD supports key-value like blocks for settings
like `audio_output` and `neighbor`. Some of these blocks can be
specified multiple times, so the following configuration:
```txt
audio_output {
device "iec958:CARD=Intel,DEV=0"
mixer_control "PCM"
name "My specific ALSA output"
type "alsa"
}
audio_output {
mixer_type "null"
name "ALSA Null"
type "alsa"
}
audio_output {
name "The Pulse"
type "pulse"
}
```
Can be inserted with:
```nix
audio_output = [
{
type = "alsa";
name = "My specific ALSA output";
device = "iec958:CARD=Intel,DEV=0";
mixer_control = "PCM";
}
{
type = "alsa";
name = "ALSA Null";
mixer_type = "null";
}
{
type = "pulse";
name = "The Pulse";
}
];
```
'';
};
@@ -226,7 +329,7 @@ in
type = lib.types.bool;
default = false;
description = ''
If set, add fluidsynth soundfont and configure the plugin.
If set, add fluidsynth soundfont `decoder` block.
'';
};
};
@@ -235,8 +338,47 @@ in
###### implementation
imports = [
(lib.mkRenamedOptionModule
[ "services" "mpd" "musicDirectory" ]
[ "services" "mpd" "settings" "music_directory" ]
)
(lib.mkRenamedOptionModule
[ "services" "mpd" "playlistDirectory" ]
[ "services" "mpd" "settings" "playlist_directory" ]
)
(lib.mkRenamedOptionModule [ "services" "mpd" "dbFile" ] [ "services" "mpd" "settings" "db_file" ])
(lib.mkRenamedOptionModule
[ "services" "mpd" "network" "listenAddress" ]
[ "services" "mpd" "settings" "bind_to_address" ]
)
(lib.mkRenamedOptionModule
[ "services" "mpd" "network" "port" ]
[ "services" "mpd" "settings" "port" ]
)
(lib.mkRemovedOptionModule
[
"services"
"mpd"
"extraConfig"
]
"services.mpd.extraConfig was replaced by the declarative services.mpd.settings option, per RFC42."
)
];
config = lib.mkIf cfg.enable {
warnings =
lib.optional
(
!(builtins.elem cfg.bind_to_address [
"localhost"
"127.0.0.1"
])
&& !cfg.openFirewall
)
"Using '${cfg.bind_to_address}' as services.mpd.settings.bind_to_address without enabling services.mpd.openFirewall, might prevent you from accessing MPD from other clients.";
# install mpd units
systemd.packages = [ pkgs.mpd ];
@@ -245,12 +387,12 @@ in
listenStreams = [
"" # Note: this is needed to override the upstream unit
(
if pkgs.lib.hasPrefix "/" cfg.network.listenAddress then
cfg.network.listenAddress
if pkgs.lib.hasPrefix "/" cfg.settings.bind_to_address then
cfg.settings.bind_to_address
else
"${
lib.optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"
}${toString cfg.network.port}"
lib.optionalString (cfg.settings.bind_to_address != "any") "${cfg.settings.bind_to_address}:"
}${toString cfg.settings.port}"
)
];
};
@@ -282,17 +424,19 @@ in
StateDirectory =
[ ]
++ lib.optionals (cfg.dataDir == "/var/lib/${name}") [ name ]
++ lib.optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [
++ lib.optionals (cfg.settings.playlist_directory == "/var/lib/${name}/playlists") [
name
"${name}/playlists"
]
++ lib.optionals (cfg.musicDirectory == "/var/lib/${name}/music") [
++ lib.optionals (cfg.settings.music_directory == "/var/lib/${name}/music") [
name
"${name}/music"
];
};
};
networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.settings.port ];
users.users = lib.optionalAttrs (cfg.user == name) {
${name} = {
inherit uid;
+5 -6
View File
@@ -8,7 +8,6 @@
let
cfg = config.services.mpdscribble;
mpdCfg = config.services.mpd;
mpdOpt = options.services.mpd;
endpointUrls = {
"last.fm" = "http://post.audioscrobbler.com";
@@ -114,11 +113,11 @@ in
host = lib.mkOption {
default = (
if mpdCfg.network.listenAddress != "any" then mpdCfg.network.listenAddress else "localhost"
if mpdCfg.settings.bind_to_address != "any" then mpdCfg.settings.bind_to_address else "localhost"
);
defaultText = lib.literalExpression ''
if config.${mpdOpt.network.listenAddress} != "any"
then config.${mpdOpt.network.listenAddress}
if config.services.mpd.settings.bind_to_address != "any"
then config.services.mpd.settings.bind_to_address
else "localhost"
'';
type = lib.types.str;
@@ -148,8 +147,8 @@ in
};
port = lib.mkOption {
default = mpdCfg.network.port;
defaultText = lib.literalExpression "config.${mpdOpt.network.port}";
default = mpdCfg.settings.port;
defaultText = lib.literalExpression "config.services.mpd.settings.port";
type = lib.types.port;
description = ''
Port for the mpdscribble daemon to search for a mpd daemon on.
+2 -2
View File
@@ -33,8 +33,8 @@ in
port = lib.mkOption {
type = lib.types.port;
default = config.services.mpd.network.port;
defaultText = lib.literalExpression "config.services.mpd.network.port";
default = config.services.mpd.settings.port;
defaultText = lib.literalExpression "config.services.mpd.settings.port";
description = "The port where MPD is listening.";
example = 6600;
};
+32 -34
View File
@@ -1,28 +1,18 @@
{ pkgs, lib, ... }:
let
track = pkgs.fetchurl {
# Sourced from http://freemusicarchive.org/music/Blue_Wave_Theory/Surf_Music_Month_Challenge/Skyhawk_Beach_fade_in
# Sourced from https://freemusicarchive.org/music/Jazz_at_Mladost_Club/Jazz_Night/Blue_bossa/
name = "Blue_Wave_Theory-Skyhawk_Beach.mp3";
url = "https://freemusicarchive.org/file/music/ccCommunity/Blue_Wave_Theory/Surf_Music_Month_Challenge/Blue_Wave_Theory_-_04_-_Skyhawk_Beach.mp3";
hash = "sha256-91VDWwrcP6Cw4rk72VHvZ8RGfRBrpRE8xo/02dcJhHc=";
name = "Blue bossa - Jazz at Miadost Club.mp3";
url = "https://files.freemusicarchive.org/storage-freemusicarchive-org/music/no_curator/Jazz_at_Mladost_Club/Jazz_Night/Jazz_at_Mladost_Club_-_07_-_Blue_bossa.mp3?download=1&name=Jazz%20at%20Mladost%20Club%20-%20Blue%20bossa.mp3";
hash = "sha256-cAG4nBuc97J3ZJc9cm/6vWTgnPL/Hfkar7cA3+89rto=";
meta.license = lib.licenses.cc-by-sa-40;
};
defaultCfg = rec {
defaultMpdCfg = {
user = "mpd";
group = "mpd";
dataDir = "/var/lib/mpd";
musicDirectory = "${dataDir}/music";
};
defaultMpdCfg = {
inherit (defaultCfg)
dataDir
musicDirectory
user
group
;
enable = true;
};
@@ -30,7 +20,7 @@ let
{
user,
group,
musicDirectory,
dataDir,
}:
{
description = "Sets up the music file(s) for MPD to use.";
@@ -38,7 +28,7 @@ let
after = [ "mpd.service" ];
wantedBy = [ "default.target" ];
script = ''
cp ${track} ${musicDirectory}
cp ${track} ${dataDir}/music
'';
serviceConfig = {
User = user;
@@ -57,7 +47,10 @@ in
{
name = "mpd";
meta = {
maintainers = with lib.maintainers; [ emmanuelrosa ];
maintainers = with lib.maintainers; [
emmanuelrosa
doronbehar
];
};
nodes = {
@@ -68,18 +61,20 @@ in
lib.mkMerge [
(mkServer {
mpd = defaultMpdCfg // {
network.listenAddress = "any";
extraConfig = ''
audio_output {
type "alsa"
name "ALSA"
mixer_type "null"
}
'';
settings = {
bind_to_address = "any";
audio_output = [
{
type = "alsa";
name = "ALSA";
mixer_type = "null";
}
];
};
openFirewall = true;
};
musicService = musicService { inherit (defaultMpdCfg) user group musicDirectory; };
musicService = musicService { inherit (defaultMpdCfg) user group dataDir; };
})
{ networking.firewall.allowedTCPPorts = [ 6600 ]; }
];
serverPulseAudio =
@@ -87,15 +82,15 @@ in
lib.mkMerge [
(mkServer {
mpd = defaultMpdCfg // {
extraConfig = ''
audio_output {
type "pulse"
name "The Pulse"
settings.audio_output = [
{
type = "pulse";
name = "The Pulse";
}
'';
];
};
musicService = musicService { inherit (defaultMpdCfg) user group musicDirectory; };
musicService = musicService { inherit (defaultMpdCfg) user group dataDir; };
})
{
services.pulseaudio = {
@@ -144,5 +139,8 @@ in
# The PulseAudio-based server is configured not to accept external client connections
# to perform the following test:
client.fail(f"{mpc} -h serverPulseAudio status")
# For inspecting these files
serverALSA.copy_from_vm("/run/mpd/mpd.conf", "ALSA")
serverPulseAudio.copy_from_vm("/run/mpd/mpd.conf", "PulseAudio")
'';
}
+13 -10
View File
@@ -232,8 +232,8 @@ stdenv.mkDerivation (finalAttrs: {
(stdenv.hostPlatform.isDarwin && lib.versionOlder stdenv.hostPlatform.darwinSdkVersion "12.0")
''
substituteInPlace src/output/plugins/OSXOutputPlugin.cxx \
--replace kAudioObjectPropertyElement{Main,Master} \
--replace kAudioHardwareServiceDeviceProperty_Virtual{Main,Master}Volume
--replace-fail kAudioObjectPropertyElement{Main,Master} \
--replace-fail kAudioHardwareServiceDeviceProperty_Virtual{Main,Master}Volume
''
+
lib.optionalString
@@ -262,17 +262,19 @@ stdenv.mkDerivation (finalAttrs: {
];
mesonFlags = [
"-Dtest=true"
"-Dmanpages=true"
"-Dhtml_manual=true"
(lib.mesonBool "test" true)
(lib.mesonBool "manpages" true)
(lib.mesonBool "html_manual" true)
]
++ map (x: "-D${x}=enabled") features_
++ map (x: "-D${x}=disabled") (lib.subtractLists features_ knownFeatures)
++ map (x: lib.mesonEnable x true) features_
++ map (x: lib.mesonEnable x false) (lib.subtractLists features_ knownFeatures)
++ lib.optional (builtins.elem "zeroconf" features_) (
"-Dzeroconf=" + (if stdenv.hostPlatform.isDarwin then "bonjour" else "avahi")
lib.mesonOption "zeroconf" (if stdenv.hostPlatform.isDarwin then "bonjour" else "avahi")
)
++ lib.optional (builtins.elem "systemd" features_) "-Dsystemd_system_unit_dir=etc/systemd/system"
++ lib.optional (builtins.elem "qobuz" features_) "-Dnlohmann_json=enabled";
++ lib.optional (builtins.elem "systemd" features_) (
lib.mesonOption "systemd_system_unit_dir" "etc/systemd/system"
)
++ lib.optional (builtins.elem "qobuz" features_) (lib.mesonEnable "nlohmann_json" true);
passthru.tests.nixos = nixosTests.mpd;
@@ -282,6 +284,7 @@ stdenv.mkDerivation (finalAttrs: {
license = lib.licenses.gpl2Only;
maintainers = with lib.maintainers; [
tobim
doronbehar
];
platforms = lib.platforms.unix;
mainProgram = "mpd";