Blurt

Breaking down the NixOS GUI setup

I am using Xmonad on my NixOS setup and somehow, configuring ibus doesn’t result to the daemon being spawned on boot automatically. This sparked a little investigation into how NixOS handles display-, window- and desktop managers :wink: with the hope that a better understanding could provide me with some insights on how to pragmatically tackle the issue I’m dealing with.

Nixos defines a display-manager.service (view by running systemctl cat display-manager.service) which handles the entire dance of firing up the login interface, in which the user is allowed to login and select the environment of choice, up to keeping the desktop- or window manager alive.

# /nix/store/*-unit-display-manager.service/display-manager.service
[Unit]
After=systemd-udev-settle.service local-fs.target acpid.service systemd-logind.service
Description=X11 Server
Wants=systemd-udev-settle.service

[Service]
Environment="LD_LIBRARY_PATH=/nix/store/*-libX11-1.6.5/lib:/nix/store/*-libXext-1.3.3/lib:/run/opengl-driver/lib"
Environment="LOCALE_ARCHIVE=/nix/store/*-glibc-locales-2.25-49/lib/locale/locale-archive"
Environment="PATH=/nix/store/*-coreutils-8.28/bin:/nix/store/*-findutils-4.6.0/bin:/nix/store/*-gnugrep-3.1/bin:/nix/store/*-gnused-4.4/bin:/nix/store/*-systemd-234/bin:/nix/store/*-coreutils-8.28/sbin:/nix/store/*-findutils-4.6.0/sbin:/nix/store/*-gnugrep-3.1/sbin:/nix/store/*-gnused-4.4/sbin:/nix/store/*-systemd-234/sbin"
Environment="SLIM_CFGFILE=/nix/store/*-slim.cfg"
Environment="SLIM_THEMESDIR=/nix/store/*-slim-theme"
Environment="TZDIR=/nix/store/*-tzdata-2016j/share/zoneinfo"
Environment="XORG_DRI_DRIVER_PATH=/run/opengl-driver/lib/dri"

X-RestartIfChanged=false


ExecStart=/nix/store/*-unit-script/bin/display-manager-start
ExecStartPre=/nix/store/*-unit-script/bin/display-manager-pre-start
Restart=always
RestartSec=200ms
StartLimitBurst=3
StartLimitInterval=30s
SyslogIdentifier=display-manager

The ExecStartPre stage of the service takes care of clearing a lock if one peeks into the scripts that is fired

#! /nix/store*-bash-*/bin/bash -e
rm -f /tmp/.X0-lock

whereas the script called in ExecStart unsurprisingly fires up the display manager

#! /nix/store/*-bash-*/bin/bash -e
exec /nix/store/*-slim-*/bin/slim

to get this train rolling.

I have mentioned display, desktop and window managers in the last few sentences without explaining what the different roles of these components are, so here follows an attempt of my simple brain to list the functions of these components along with some examples:

  • display managers: tools that present login interfaces and provide methods to select the wanted environment such as
    • lightdm
    • sddm
    • slim
  • window managers: tools that just handle windows such as
    • XMonad
    • i3
    • wmii
  • desktop managers: tools that handle entire full-blown desktop environments such as
    • gnome
    • KDE
    • XFCE

Within NixOS’s display-managers/default.nix file, an xsession function is defined which is quite interesting to dissect.

Dissecting xsession

After successfully getting past the display manager by providing the correct login credentials and selecting a valid window or desktop manager, X server is fired up. The xsession function provides some insights into the steps taken in setting up this X server session.

For simplicity’s sake, the creative license is taken to display bash snippets instead of the nix snippets wherever “sensible”1. The xsession function discussed, basically produces a bash script by means of the writeScript function. In several cases, wherever the nix code is crucial to the understanding of the snippet, the entire nix snippet is presented but in any case the octocat :octocat: emoji’s are links to the original nix code for reference’s sake :wink:.

:point_right: When called with the args $1 and $2 where $1 constitutes an absolute path (i.e.: starts with the character /), execute $1 with $2 as an argument :octocat:.

# Expected parameters:
#   $1 = <desktop-manager>+<window-manager>
# Actual parameters (FIXME):
# SDDM is calling this script like the following:
#   $1 = /nix/store/xxx-xsession (= $0)
#   $2 = <desktop-manager>+<window-manager>
# SLiM is using the following parameter:
#   $1 = /nix/store/xxx-xsession <desktop-manager>+<window-manager>
# LightDM keeps the double quotes:
#   $1 = /nix/store/xxx-xsession "<desktop-manager>+<window-manager>"
# The fake/auto display manager doesn't use any parameters and GDM is
# broken.
# If you want to "debug" this script don't print the parameters to stdout
# or stderr because this script will be executed multiple times and the
# output won't be visible in the log when the script is executed for the
# first time (e.g. append them to a file instead)!
# All of the above cases are handled by the following hack (FIXME).
# Since this line is *very important* for *all display managers* it is
# very important to test changes to the following line with all display
# managers:
if [ "${1:0:1}" = "/" ]; then eval exec "$1" "$2" ; fi

:snowflake: Note when looking at the Nix code (:octocat:), that ${ is a special sequence in Nix and has to be escaped by prefixing it with '' to become ''${ as pointed out in the thread to the commit that introduced this change and the documentation2.

:point_right: Optionally log to journal using systemd-cat provided that displayManager.logToJournal is set :octocat:.

if [ -z "$_DID_SYSTEMD_CAT" ]; then
  _DID_SYSTEMD_CAT=1 \
    exec $SYSTEMD_PATH/bin/systemd-cat -t xsession -- \
    "$0" "$@"
fi

:point_right: Source /etc/profile and change directory into $HOME :octocat:.

. /etc/profile
cd "$HOME"

:point_right: Ensure sessionType is an empty string if the first argument $1 is default, otherwise sessionType is set to the value of $1 :octocat:.

# The first argument of this script is the session type.
sessionType="$1"
if [ "$sessionType" = default ]; then sessionType=""; fi

:point_right: Log errors to ~/.xsession-errors if the NixOS displayManager options logXsession and logToJournal are not set :octocat:.

exec > ~/.xsession-errors 2>&1

:point_right: Start a DBus session provided that the NixOS option services.xserver.startDbusSession is set to true. :octocat:.

if test -z "$DBUS_SESSION_BUS_ADDRESS"; then
  exec ${pkgs.dbus.dbus-launch} --exit-with-session "$0" "$sessionType"
fi

:point_right: Start pulseaudio :octocat:

# Start PulseAudio if enabled.
optionalString (config.hardware.pulseaudio.enable) ''
  ${optionalString (!config.hardware.pulseaudio.systemWide)
    "${config.hardware.pulseaudio.package.out}/bin/pulseaudio --start"
  }

  # Publish access credentials in the root window.
  ${config.hardware.pulseaudio.package.out}/bin/pactl load-module module-x11-publish "display=$DISPLAY"
''

:point_right: Inform systemd about $DISPLAY and $XAUTHORITY :octocat:.

# Tell systemd about our $DISPLAY and $XAUTHORITY.
# This is needed by the ssh-agent unit.
#
# Also tell systemd about the dbus session bus address.
# This is required by user units using the session bus.
$SYSTEMCTL_PATH/bin/systemctl --user import-environment DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS

:point_right: Load Xdefaults from ~/.Xresources and ~/.Xdefaults :octocat:.

# Load X defaults.
${xorg.xrdb}/bin/xrdb -merge ${xresourcesXft}
if test -e ~/.Xresources; then
    ${xorg.xrdb}/bin/xrdb -merge ~/.Xresources
elif test -e ~/.Xdefaults; then
    ${xorg.xrdb}/bin/xrdb -merge ~/.Xdefaults
fi

:point_right: Handle KDE voodoo :see_no_evil: :octocat:

# Speed up application start by 50-150ms according to
# http://kdemonkey.blogspot.nl/2008/04/magic-trick.html
rm -rf "$HOME/.compose-cache"
mkdir "$HOME/.compose-cache"

# Work around KDE errors when a user first logs in and
# .local/share doesn't exist yet.
mkdir -p "$HOME/.local/share"

:point_right: Release logToJournal lock :octocat:.

unset _DID_SYSTEMD_CAT

:point_right: Run displayManager.sessionCommands :octocat:.

:point_right: Load ~/.xprofile :octocat:.

# Allow the user to execute commands at the beginning of the X session.
if test -f ~/.xprofile; then
    source ~/.xprofile
fi

:point_right: Start graphical-session systemd target :octocat:.

# Start systemd user services for graphical sessions
$SYSTEMD_PATH/bin/systemctl --user start graphical-session.target

Note that $SYSTEMD_PATH is just a substitution for the real systemd.package path which is expanded into this string by Nix :wink:.

:point_right: Honor ~/.xsession :octocat:.

# Allow the user to setup a custom session type.
if test -x ~/.xsession; then
    exec ~/.xsession
else
    if test "$sessionType" = "custom"; then
        sessionType="" # fall-thru if there is no ~/.xsession
    fi
fi

:point_right: Set desktopManager and windowManager where windowManager is set to the phrase contained by sessionType without the all characters from the beginning up to the plus sign while desktopManager is set to sessionType excluding all characters from the end of the phrase up to the plus sign :wink: :octocat:

# The session type is "<desktop-manager>+<window-manager>", so
# extract those (see:
# http://wiki.bash-hackers.org/syntax/pe#substring_removal).
windowManager="''${sessionType##*+}"
: ''${windowManager:=${cfg.windowManager.default}}
desktopManager="''${sessionType%%+*}"
: ''${desktopManager:=${cfg.desktopManager.default}}

:point_right: Start the window- and desktop manager :octocat:.

''# Start the window manager.
case "$windowManager" in
  ${concatMapStrings (s: ''
    (${s.name})
      ${s.start}
      ;;
  '') wm}
  (*) echo "$0: Window manager '$windowManager' not found.";;
esac

# Start the desktop manager.
case "$desktopManager" in
  ${concatMapStrings (s: ''
    (${s.name})
      ${s.start}
      ;;
  '') dm}
  (*) echo "$0: Desktop manager '$desktopManager' not found.";;
esac
''

:point_right: Update DBus environment :octocat:.

${optionalString cfg.updateDbusEnvironment ''
  ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
''}

:point_right: Wait for the X session process to terminate, then stop stop graphical-session systemd target and exit :octocat:.

test -n "$waitPID" && wait "$waitPID"

$SYSTEMD_PATH/bin/systemctl --user stop graphical-session.target

exit 0

Where to handle daemon spawning

In our case there seem to exist several places where one could potentially start the ibus daemon.

  • displayManager.sessionCommands
  • ~/.xprofile
  • a systemd unit that starts after graphic-session.target
  • ~/.xsession
  • XMonad configuration

In my case, I opted for a systemd unit, which I defined in my nixos configuration as follows:

systemd.user.services.ibus-daemon = {
  enable = true;
  wantedBy = [
    "multi-user.target"
    "graphical-session.target"
  ];
  description = "IBus daemon";
  script = "${pkgs.ibus-with-plugins}/bin/ibus-daemon";
  serviceConfig = {
    Restart = "always";
    StandardOutput = "syslog";
  };
};
  1. Just a personal call. Since the xsession script happens to be a bash script it felt reasonable to focus on the bash code to understand the purpose and behavior rather than tunnel-visioning on the nix part, however enticing :stuck_out_tongue_closed_eyes:

  2. Since ${ and '' have special meaning in indented strings, you need a way to quote them. ${ can be escaped by prefixing it with '' (that is, two single quotes), i.e., ''${. '' can be escaped by prefixing it with ', i.e., '''. Finally, linefeed, carriage-return and tab characters can be written as ''\n, ''\r, ''\t