How is XDG_DATA_DIRS set for some apps?

I had to set environment.sessionVariables.XDG_DATA_DIRS to include pkgs.gsettings-desktop-schemas path in order to gsettings to work in bash. Without doing that, that envvar has a few paths, but not the one for pkgs.gsettings-desktop-schemas.
But, even before setting that envvar, some apps (like the dconf-editor, but also swaynotificationcenter) had no issue accessing the schemas.

Anyone knows how did it work for some apps but not others?

We do not have global location for schemas for the same reason we do not have other FHS structures like /usr/lib: incompatibilities between multiple different versions and conflicts. Instead, each program is responsible for pulling in the schemas it needs, see Packaging GNOME applications section of the Nixpkgs manual.

Of course, this does not work for programs that expect to see every schema installed on the system (e.g. dconf-editor). But since Nix has no concept that would match “installed” of traditional package managers that cannot really be implemented in the first place.

The closest thing would be packages linked by one of the profiles (see this comment for how to make gsettings program see those schemas) but that will lack schemas of (transitive) dependencies.

Thanks for the overview!
The weird thing for me is that dconf-editor was finding the schemas after I installed pkgs.gsettings-desktop-schemas (but only after rebooting).
Also, xdg-desktop-portal was also finding them since Firefox and other apps changed the color scheme when I toggled it via swaync (which was just calling set with gsettings).
But using gsettings on kitty yielded nothing.

That might be a failure of measurement. Installing schemas using regular options like environment.systemPackages will not be able to put schemas to a place where they could be visible to apps. And installing gsettings-desktop-schemas should not be necessary in the first place because dconf-editor has access to gsettings-desktop-schemas out of the box from gtk3 propagating it, and then being picked up by the wrapper.

Both xdg-desktop-portal and swaynotificationcenter are wrapped in the same way. And since wrapping relies on environment variables and those are usually inherited by child processes, gsettings command executed by swaync has access to those schemas.

And while kitty also has issues with wrapper environment leaking, it is not a GTK app so schemas are not among the leaked.

Oh, now it all makes sense! I did not knew about wrapGAppsHook and somehow missed it on your first link about packaging gnome apps! Thanks a lot for the explanation!
Is there any other proper way to enable shell scripts to be able to access the schemas other than setting environment.sessionVariables.XDG_DATA_DIRS directly?

There is intentionally no way to do that globally. There are essentially two proper methods:

Packaging the shell script as Nix derivation

(pkgs.writeShellScriptBin "toggle-color-scheme" ''
  XDG_DATA_DIRS="${pkgs.glib.getSchemaPath pkgs.glib}" "${pkgs.glib}/bin/gsettings" set org.gnome.desktop.interface color-scheme prefer-dark
'')

You can just install the package created by the expression (e.g. through environment.systemPackages or by storing it to a variable and then using ${variable}/bin/toggle-color-scheme in your swaync config (if it is generated in Nix)).

There are also other variations of the method:

Wrapping a separate script

#!/usr/bin/env bash
gsettings set org.gnome.desktop.interface color-scheme prefer-dark
(pkgs.runCommand
  "toggle-color-scheme"
  {
    nativeBuildInputs = [
      pkgs.makeWrapper
    ];
  }
  ''
    mkdir -p "$out/bin"
    cp -r "${./toggle-color-scheme.sh}" "$out/bin/toggle-color-scheme"

    # Replace the interpreter with static path not relying on PATH.
    patchShebangs "$out/bin/toggle-color-scheme"

    # Wrap the script with environment variables necessary for finding dependencies.
    wrapProgram "$out/bin/toggle-color-scheme" \
      --prefix PATH : "${lib.makeBinPath [ pkgs.glib ]}" \
      --prefix XDG_DATA_DIRS : "${pkgs.glib.getSchemaPath pkgs.glib}"
  '')

This might be useful if you want to package a third-party script or you want to use the script outside of NixOS as well.

If you just need PATH fixed, resholved might be less tedious and error-prone.

Using nix-shell as an interpreter

This is useful if you want something that is more immediately runnable without having to jump through packaging:

#! /usr/bin/env nix-shell
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/842d9d80cfd4560648c785f8a4e6f3b096790e19.tar.gz -i bash -p glib -p gsettings-desktop-schemas
export XDG_DATA_DIRS="$GSETTINGS_SCHEMAS_PATH"
gsettings set org.gnome.desktop.interface color-scheme prefer-dark

The above shebang runs the script as if it was run in a shell created with something like pkgs.mkShell { buildInputs = [ pkgs.glib pkgs.gsettings-desktop-schemas ]; }. The glib setup hook populates the Nixpkgs-specific GSETTINGS_SCHEMAS_PATH environment variable, which you can use in XDG_DATA_DIRS to make gsettings command find the schemas.

I also pin the Nixpkgs revision to ensure long term reproducibility.

This option will be a lot slower than the first one since, unless you abandon reproducibility and use system-wide NIX_PATH, Nixpkgs will need to be fetched at minimum the first time the script is executed. Then Nix will need to evaluate the Nix environment derivation on each execution. There are some links on how to improve performance on the unofficial wiki.

See also Reproducible interpreted scripts — nix.dev documentation

2 Likes

Thanks a lot for the overview!
So, the script used to toggle the color scheme is used by swaynotificationcenter, which works fine already due to the wrapper you mentioned. The other script (the one that was not working) is launched by Hyprland at login and uses gsettings monitor to monitor changes to the color scheme and react.

Since at the moment I use my hyprland dotfiles on non-nix systems, I would prefer to keep the scripts general and contained in my hyprland dotfiles.

Another thing that works is adding the wrapGAppsHook to hyprland with an overlay. That lets me keep my hyprland & friends dotfiles generic. Is this a good approach?
The downside is that the XDG* envs are propagated to child processes of hyprland (every app I launch, pretty much).

Yeah, that’s good enough reason for me to avoid that – in the rare case an app is not wrapped when it should be, and incompatible schema is passed, it might even crash. Plus you will need to recompile the overlaid package locally.

But if you do not want to go the way of wrapping a separate script and are okay with the downsides, it will probably work.

1 Like