dconf: support configuring specific user databases (#6301)

By default, dconf uses $XDG_CONFIG_HOME/dconf/user as the user database, but this can be changed by specifying user-db:<name> in a profile file and setting the DCONF_PROFILE environment variable to that profile. One may want to use different user databases for different DE/WMs to avoid collision.

Currently the module invokes dconf without touching DCONF_PROFILE, which means that 1) it is unable to configure multiple different user databases, and 2) the behavior of activation script will be affected by the DCONF_PROFILE environment variable when it is invoked, possibly leading to undesired results.

This PR adds a dconf.databases option, so that settings under dconf.databases.<name> will be written to $XDG_CONFIG_HOME/dconf/<name>. The old dconf.settings option is left as-is to avoid breaking compatibility.
This commit is contained in:
DDoSolitary
2025-11-30 21:37:17 +08:00
committed by GitHub
parent b1bb534c17
commit edbb012a21
4 changed files with 183 additions and 55 deletions

View File

@@ -17,15 +17,29 @@ let
# The dconf keys managed by this configuration. We store this as part of the
# generation state to be able to reset keys that become unmanaged during
# switch.
stateDconfKeys = pkgs.writeText "dconf-keys.json" (
builtins.toJSON (
lib.concatLists (
lib.mapAttrsToList (
dir: entries: lib.mapAttrsToList (key: _: "/${dir}/${key}") entries
) cfg.settings
mkStateDconfKeys =
nameSuffix: settings:
pkgs.writeText "dconf-keys${nameSuffix}.json" (
builtins.toJSON (
lib.concatLists (
lib.mapAttrsToList (dir: entries: lib.mapAttrsToList (key: _: "/${dir}/${key}") entries) settings
)
)
)
);
);
databases =
lib.optional (cfg.settings != { }) {
dconfProfile = null;
stateDconfKeys = mkStateDconfKeys "" cfg.settings;
inherit (cfg) settings;
}
++ lib.mapAttrsToList (name: value: {
dconfProfile = pkgs.writeText "dconf-profile-${name}" ''
user-db:${name}
'';
stateDconfKeys = mkStateDconfKeys "-${name}" value;
settings = value;
}) cfg.databases;
in
{
@@ -81,73 +95,87 @@ in
to convert dconf database dumps into compatible Nix expression.
'';
};
databases = lib.mkOption {
type = with types; attrsOf (attrsOf (attrsOf lib.hm.types.gvariant));
default = { };
description = ''
Settings to write to specific dconf user databases.
See [](#opt-dconf.settings) for details.
'';
};
};
};
config = lib.mkIf (cfg.enable && cfg.settings != { }) {
config = lib.mkIf (cfg.enable && databases != [ ]) {
# Make sure the dconf directory exists.
xdg.configFile."dconf/.keep".source = builtins.toFile "keep" "";
home.extraBuilderCommands = ''
mkdir -p $out/state/
ln -s ${stateDconfKeys} $out/state/${stateDconfKeys.name}
'';
''
+ lib.concatMapStrings (db: ''
ln -s ${db.stateDconfKeys} $out/state/${db.stateDconfKeys.name}
'') databases;
home.activation.dconfSettings = lib.hm.dag.entryAfter [ "installPackages" ] (
let
iniFile = pkgs.writeText "hm-dconf.ini" (toDconfIni cfg.settings);
lib.concatMapStrings (
db:
let
iniFile = pkgs.writeText "hm-dconf.ini" (toDconfIni db.settings);
statePath = "state/${stateDconfKeys.name}";
statePath = "state/${db.stateDconfKeys.name}";
cleanup = pkgs.writeShellScript "dconf-cleanup" ''
set -euo pipefail
cleanup = pkgs.writeShellScript "dconf-cleanup" ''
set -euo pipefail
${config.lib.bash.initHomeManagerLib}
${config.lib.bash.initHomeManagerLib}
PATH=${
lib.makeBinPath [
pkgs.dconf
pkgs.jq
]
}''${PATH:+:}$PATH
PATH=${
lib.makeBinPath [
pkgs.dconf
pkgs.jq
]
}''${PATH:+:}$PATH
oldState="$1"
newState="$2"
oldState="$1"
newState="$2"
# Can't do cleanup if we don't know the old state.
if [[ ! -f $oldState ]]; then
exit 0
# Can't do cleanup if we don't know the old state.
if [[ ! -f $oldState ]]; then
exit 0
fi
# Reset all keys that are present in the old generation but not the new
# one.
jq -r -n \
--slurpfile old "$oldState" \
--slurpfile new "$newState" \
'($old[] - $new[])[]' \
| while read -r key; do
verboseEcho "Resetting dconf key \"$key\""
run $DCONF_DBUS_RUN_SESSION dconf reset "$key"
done
'';
envCommand = lib.optionalString (db.dconfProfile != null) "env DCONF_PROFILE=${db.dconfProfile}";
in
''
if [[ -v DBUS_SESSION_BUS_ADDRESS ]]; then
export DCONF_DBUS_RUN_SESSION="${envCommand}"
else
export DCONF_DBUS_RUN_SESSION="${pkgs.dbus}/bin/dbus-run-session --dbus-daemon=${pkgs.dbus}/bin/dbus-daemon ${envCommand}"
fi
# Reset all keys that are present in the old generation but not the new
# one.
jq -r -n \
--slurpfile old "$oldState" \
--slurpfile new "$newState" \
'($old[] - $new[])[]' \
| while read -r key; do
verboseEcho "Resetting dconf key \"$key\""
run $DCONF_DBUS_RUN_SESSION dconf reset "$key"
done
'';
in
''
if [[ -v DBUS_SESSION_BUS_ADDRESS ]]; then
export DCONF_DBUS_RUN_SESSION=""
else
export DCONF_DBUS_RUN_SESSION="${pkgs.dbus}/bin/dbus-run-session --dbus-daemon=${pkgs.dbus}/bin/dbus-daemon"
fi
if [[ -v oldGenPath ]]; then
${cleanup} \
"$oldGenPath/${statePath}" \
"$newGenPath/${statePath}"
fi
if [[ -v oldGenPath ]]; then
${cleanup} \
"$oldGenPath/${statePath}" \
"$newGenPath/${statePath}"
fi
run $DCONF_DBUS_RUN_SESSION ${pkgs.dconf}/bin/dconf load / < ${iniFile}
run $DCONF_DBUS_RUN_SESSION ${pkgs.dconf}/bin/dconf load / < ${iniFile}
unset DCONF_DBUS_RUN_SESSION
''
unset DCONF_DBUS_RUN_SESSION
''
) databases
);
};
}

View File

@@ -27,6 +27,7 @@ let
standalone-flake-basics = runTest ./standalone/flake-basics.nix;
standalone-specialisation = runTest ./standalone/specialisation.nix;
standalone-standard-basics = runTest ./standalone/standard-basics.nix;
dconf = runTest ./standalone/dconf.nix;
};
in
tests

View File

@@ -0,0 +1,22 @@
{ pkgs, ... }:
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "25.11"; # Please read the comment before changing.
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
dconf.settings = {
foo = {
bar = 42;
};
};
dconf.databases.custom = {
foo1 = {
bar1 = 42;
};
};
}

View File

@@ -0,0 +1,77 @@
{ pkgs, ... }:
{
name = "dconf";
meta.maintainers = [ pkgs.lib.maintainers.rycee ];
nodes.machine = {
imports = [ "${pkgs.path}/nixos/modules/installer/cd-dvd/channel.nix" ];
virtualisation.memorySize = 2048;
users.users.alice = {
isNormalUser = true;
description = "Alice Foobar";
password = "foobar";
uid = 1000;
};
programs.dconf = {
enable = true;
profiles.custom = pkgs.writeText "dconf-profile-custom" ''
user-db:custom
'';
};
};
testScript = ''
start_all()
machine.wait_for_unit("network.target")
machine.wait_for_unit("multi-user.target")
home_manager = "${../../..}"
def login_as_alice():
machine.wait_until_tty_matches("1", "login: ")
machine.send_chars("alice\n")
machine.wait_until_tty_matches("1", "Password: ")
machine.send_chars("foobar\n")
machine.wait_until_tty_matches("1", "alice\\@machine")
def logout_alice():
machine.send_chars("exit\n")
def alice_cmd(cmd):
return f"su -l alice --shell /bin/sh -c $'export XDG_RUNTIME_DIR=/run/user/$UID ; {cmd}'"
def succeed_as_alice(cmd):
return machine.succeed(alice_cmd(cmd))
def fail_as_alice(cmd):
return machine.fail(alice_cmd(cmd))
# Create a persistent login so that Alice has a systemd session.
login_as_alice()
# Set up a home-manager channel.
succeed_as_alice(" ; ".join([
"mkdir -p /home/alice/.nix-defexpr/channels",
f"ln -s {home_manager} /home/alice/.nix-defexpr/channels/home-manager"
]))
succeed_as_alice("nix-shell \"<home-manager>\" -A install")
succeed_as_alice("cp ${./dconf-home.nix} /home/alice/.config/home-manager/home.nix")
succeed_as_alice("home-manager switch")
succeed_as_alice("test -e /home/alice/.config/dconf/user")
actual = succeed_as_alice("dconf dump /")
expected = """[foo]
bar=42
"""
assert actual == expected, "invalid content in dconf database \"user\""
succeed_as_alice("test -e /home/alice/.config/dconf/custom")
actual = succeed_as_alice("DCONF_PROFILE=custom dconf dump /")
expected = """[foo1]
bar1=42
"""
assert actual == expected, "invalid content in dconf database \"custom\""
'';
}