diff --git a/modules/misc/dconf.nix b/modules/misc/dconf.nix index ec2484659..de8da53c3 100644 --- a/modules/misc/dconf.nix +++ b/modules/misc/dconf.nix @@ -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 ); }; } diff --git a/tests/integration/default.nix b/tests/integration/default.nix index bceacaaf7..7c6e3f024 100644 --- a/tests/integration/default.nix +++ b/tests/integration/default.nix @@ -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 diff --git a/tests/integration/standalone/dconf-home.nix b/tests/integration/standalone/dconf-home.nix new file mode 100644 index 000000000..dd15a55f4 --- /dev/null +++ b/tests/integration/standalone/dconf-home.nix @@ -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; + }; + }; +} diff --git a/tests/integration/standalone/dconf.nix b/tests/integration/standalone/dconf.nix new file mode 100644 index 000000000..6b949db84 --- /dev/null +++ b/tests/integration/standalone/dconf.nix @@ -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 \"\" -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\"" + ''; +}