Diving Into NixOS (Part 3): Lightweight Startup With xinit

Brief case study

In this post, we continue on directly from part 2. I would recommend reading the previous part to gain an understanding of the Nix ecosystem if you are not already familiar with it. In the spirit of continuing the train of thought, instead of plainly listing some of the configuration options and explaining them without any context, I thought it would be more interesting to examine the steps I took to configure xinit.

Configurable services

As mentioned, NixOS has a wealthy host of configuration options that can be set in the configuration.nix file. The set of options that are of interest to us are the services. As far as I'm concerned, each of these is more-or-less mapped to the setup of one-or-more systemd services. Some examples include CUPS, for printing; or GitLab's CI runner, for dedicated runners; or most importantly in our case, a service for managing X11.

X server options

When installing NixOS, the default configuration.nix will probably have already filled-out some sane settings for the xserver. These defaults allow users to log in to the system after a fresh install via a display manager before it throws you into the desktop environment.

  # ...

  # Enable the X11 windowing system.
  services.xserver.enable = true;
  services.xserver.layout = "us";
  services.xserver.xkbOptions = "eurosign:e";

  # Enable touchpad support.
  services.xserver.libinput.enable = true;

  # Enable the KDE Desktop Environment.
  services.xserver.displayManager.sddm.enable = true;
  services.xserver.desktopManager.plasma5.enable = true;

  # ...

Getting Nix to play nicely with xinit

For many, this may just as well be exactly what they want/need. If you're still reading, however, chances are you would like to avoid using a bloated desktop environment altogether and keep things light. This can be achieved using a combination of standard shell login and xinit to boot into a window manager.

  1. Log in to tty using username and password.
  2. Run startx.

For the most part, configuring xinit is quite simple - the Arch Wiki is our ever helpful resource in times like this.

# .xserverrc
#!/usr/bin/env sh
exec /run/current-system/sw/bin/Xorg -nolisten tcp -nolisten local "$@" "vt""$XDG_VTNR"
# .xinitrc
#!/usr/bin/env sh
exec bspwm

Note a couple of things:

  • Remember that all most programs, libraries, etc. are symlinked under respective directories under /run/current-system/sw/, so if you need to specify the full path to a binary, that would be the first place to look.
  • In the example, I start bspwm as my window manager (wm), but you could use any wm you choose as long as they are built for X.
  • Naturally the xorg.xinit, bspwm, and sxhkd (bspwm requirement) packages will need to be added to the environment.systemPackages list in the Nix configuration in order to make them available to all users.

Now because NixOS typically uses systemd to start X, unlike other Linux distributions, all the system configuration files for Xorg (modules that define drivers for graphics, input, etc.) are not available in a central directory. This means that trying to just run startx after preparing our .xserverrc and .xinitrc will not work.

My first intuition was that I should symlink additional directories of the filesystem hierarchy that included the appropriate Xorg modules. This can be done in a single line via the environment.pathsToLink option.

environment.pathsToLink = [ "/etc" ];

Unfortunately, although it works, it ends up symlinking the contents of other packages with /etc/ subdirectories. We refer to this as polluting the filesystem which ends up exposing more files than we need and leaving the system configuration dependent in a state that is no longer atomic - an unsatisfactory solution.

With thanks to the friendly NixOS community, I discovered that a similar effect could be achieved by disabling services.xserver.autorun and enabling services.xserver.exportConfiguration.

Whether to symlink the X server configuration under /etc/X11/xorg.conf.

By enabling the option, our environment still polluted, but only with the Xorg modules, allowing xinit to read the configuration from the expected directory and launch correctly!

Through this experience and community feedback, I learned a few additional things about X11. For example, startx can receive a number of options on the command line, such as the program to execute when X is initialized, i.e. startx /run/current-system/sw/bin/bspwm. Another interesting point was that unless it is necessary, it is good practice to leave the resolution parameter in the xserver options unset as xrandr can detect the appropriate monitor resolution for us.

With a single, key configuration option, we were able to bend NixOS in a way that would allow us to use a hybrid approach to logging in and starting X. Ideally, this would be implemented as a dummy display manager (it looks like this is an officially supported option on the unstable NixPkgs channel - not available in 18.09 Jellyfish at time of writing - since #47773) to avoid pollution of the filesystem with extraneous symlinks.

In the final part of the series, we will take a look at how my development workflow has greatly benefited from using nix shell in conjunction with a couple of other utilities.

Helpful links