Manage your $HOME with Nix

Posted on July 2, 2017

TLDR: Check out Home Manager to manage your home directory like you do your NixOS system as a whole.


If you are a happy NixOS user like me you’ve probably had the thought that it would be cool to manage your installed software, dotfiles, X session, user services, and whatnot using the same type of declarative description as you do for your system as a whole. Some work already exists to do this, at least partially. Most ambitious to date is probably the nixuser project that unfortunately has been languishing for some time as a Nixpkgs pull request. It is nice since it attempts to solve all the things listed above.

Unfortunately nixuser wasn’t a good fit for me since I primarily use the stable version of NixOS and it seemed to me a big job to backport nixuser to NixOS 17.03. Less ambitious projects and setups that focus only on managing dotfiles are more readily accessible such as nix-home and Luke Clifton’s nix-config. These didn’t satisfy me either since I really want to include goodies such as user services.

So I never managed to reap the benefits of setting up my user environment using Nix. In particular, I wanted something running on NixOS 17.03 that would let me use the full power of the NixOS module system. Basically I want to start from a file user-configuration.nix:

{ lib, pkgs, ... }: { }

and add stuff in the result set to describe my user environment, such as the installed packages, dotfiles, and so on.

All this was a thorn in my side for a long time and eventually I had a few days free early January 2017 to put together Home Manager. This is a set of NixOS modules for managing a user environment and a tool called home-manager that activates a user configuration and does some other related operations. Home Manager therefore resemble nixuser in its structure and I think it is fair to say that the primary difference is that Home Manager is developed in its own repository and only uses Nixpkgs as a source of packages and library of useful Nix functions. A secondary benefit of this is that it appears to function reasonably well under other GNU/Linux distros.

While Home Manager still is a bit rough around the edges I have successfully used and improved it for a number of months now and a number of brave individuals have tried it out without any major issues (as far as I know). Special thanks to dermetfan, elitak, league, suolrihm, and therealpxc for reporting issues and even contributing pull requests!

In the following sections I’ll provide a more detailed outline of the types of configurations I wanted to support and how each appears in a Home Manager context.

Installed packages

For whatever reason it really irks me to do nix-env -iA whatever (not to mention nix-env -i whatever) for each package I want to have installed in my user environment. Thus, soon after getting used to Nix I started keeping the packages I wanted in a custom buildEnv. Having such a meta package allowed me to easily install all my packages in one go whenever I needed, like when creating a new container.

It was therefore important to me that Home Manager made declarative package management as easy as possible. For most packages it should be sufficient to use the home.packages option. For example, you could add

home.packages = [
  pkgs.anki
  pkgs.autorandr
  …
  pkgs.xscreensaver
  pkgs.youtube-dl
];

to your configuration. After the configuration is activated these packages will be available in your user profile.

Actually, since I have multiple user environments it is nice to have a file user-common.nix that configures the packages that should be available everywhere:

{ pkgs, ... }:
{
  home.packages = [
    pkgs.atool
    pkgs.elinks
    …
    pkgs.unzip
    pkgs.zip
  ];
}

and in user-configuration.nix add

imports = [ ./user-common.nix ];

to merge the home.packages configurations. The import functionality of the NixOS module system really helps in keeping your configurations compositional so I refuse to give it up when setting up my user environment.

Some packages require a bit of extra configuration, like specifying a list of plugins. In such cases it is more convenient to have a module for the program that contains such options. For example, in my configuration I want Eclipse, Emacs, Firefox, and Texlive with extra goodies enabled:

programs.eclipse = {
  enable = true;
  jvmArgs = [ "-Xmx1024m" ];
  plugins = with pkgs.eclipses.plugins; [
    acejump
    autodetect-encoding
    …
    testng
    yedit
  ];
};

programs.emacs = {
  enable = true;
  extraPackages = epkgs: [
    epkgs.ac-haskell-process
    epkgs.ac-octave
    …
    epkgs.yaml-mode
    epkgs.yasnippet
  ];
};

programs.firefox = {
  package = pkgs.firefox-esr;
  enableAdobeFlash = true;
  enableGoogleTalk = true;
};

programs.texlive = {
  enable = true;
  extraPackages = tpkgs: {
    inherit (tpkgs)
      algorithms
      babel-swedish
      …
      ticket
      wrapfig
      ;
  };
};

Quite nice! Although I do wish to use a more consistent way of defining extra packages; here there are four modules with four different ways of hooking up plugins.

Dotfiles

For dotfiles I thought it would be nice if they could work similar to the environment.etc option in NixOS. So it should, for example, be possible to create a GHCi initialization file by having

home.file.".ghci".text = ''
  :set prompt "λ> "
'';

or

home.file.".ghci".source = ./dot-ghci;

in the configuration. Of course, if there are nice ready-made modules for common programs then that would be extra nice. Git, for example, has some well-known configuration options so I should be able to go

programs.git = {
  enable = false;
  userName = "Robert Helgesson";
  userEmail = "robert@example.org";
  signing.key = "9E65DC86";
  signing.signByDefault = true;
  aliases = {
    co = "checkout";
  };
  extraConfig = {
    core = {
      editor = "emacs";
      whitespace = "trailing-space,space-before-tab";
    };
  };
};

and have it generate a ~/.gitconfig file. Or even better, it should be possible to put

programs.git = {
  userName = "Robert Helgesson";
  signing.key = "9E65DC86";
  signing.signByDefault = true;
  aliases = {
    co = "checkout";
  };
  extraConfig = {
    core = {
      editor = "emacs";
      whitespace = "trailing-space,space-before-tab";
    };
  };
};

in user-common.nix and then go

program.git = {
  enable = true;
  userEmail = "robert+home@example.org";
};

in my personal configuration and

program.git = {
  enable = true;
  userEmail = "robert+work@example.org";
};

in my work configuration. In user environments where Git is not needed I can still import user-common.nix since the programs.git.enable option will default to false. Fortunately, though the magic of NixOS modules this all works as expected in Home Manager.

Note, as always with Nix, care needs to be taken to not put secret information in files that are written to the Nix store. A solution to this is hopefully forthcoming in the future.

Dotfile less configurations

Some software is not configured though individual files, which makes it hard to manage through some tool that only knows about dotfiles. Home Manager is also not perfectly suited for such configuration schemes, but in some cases it is still able to do something reasonable. For example, I use Gnome Terminal and wanted to configure it through Home Manager but Gnome Terminal is using dconf as a configuration backend and dconf uses a single on-disk file for all applications.

Fortunately, in the dconf case there is a command line tool that is able to import an INI file file into the dconf database. This made it possible to write a module in Home Manager such that the configuration

programs.gnome-terminal = {
  enable = true;
  showMenubar = false;
  profile = {
    "5ddfe964-7ee6-4131-b449-26bdd97518f7" = {
      default = true;
      visibleName = "Tomorrow Night";
      cursorShape = "ibeam";
      font = "DejaVu Sans Mono 8";
      showScrollbar = false;
      colors = {
        foregroundColor = "rgb(197,200,198)";
        palette = [
          "rgb(0,0,0)" "rgb(145,34,38)"
          "rgb(119,137,0)" "rgb(174,123,0)"
          "rgb(103,123,192)" "rgb(104,42,155)"
          "rgb(43,102,81)" "rgb(146,149,147)"
          "rgb(102,102,102)" "rgb(204,102,102)"
          "rgb(181,189,104)" "rgb(240,198,116)"
          "rgb(140,152,191)" "rgb(178,148,187)"
          "rgb(138,190,183)" "rgb(236,235,236)"
        ];
        boldColor = "rgb(138,186,183)";
        backgroundColor = "rgb(29,31,33)";
      };
    };
  };
};

will work as expected.

User services

NixOS uses systemd and its user services is really a killer feature for me. User service management must therefore be part of any tool that claims to manage my user environment. So Home Manager of course does this and from a user perspective it is very similar to the NixOS system services configuration.

As a simple example, to start the GnuPG key management daemon I have

services.gpg-agent = {
  enable = true;
  defaultCacheTtl = 1800;
  enableSshSupport = true;
};

in my configuration. Internally this will create a socket activated systemd user service that ensures that the agent will be started when needed.

As a module writer the systemd configuration options are somewhat different from the NixOS systemd configuration. Put simply, in Home Manager the systemd options are a 1-1 mapping to the systemd INI format. We can see this from the implementation of the gpg-agent service unit, which is a direct translation of the upstream GnuPG service:

systemd.user.services.gpg-agent = {
  Unit = {
    Description = "GnuPG cryptographic agent and passphrase cache";
    Documentation = "man:gpg-agent(1)";
    Requires = "gpg-agent.socket";
    After = "gpg-agent.socket";
    # This is a socket-activated service:
    RefuseManualStart = true;
  };

  Service = {
    ExecStart = "${pkgs.gnupg}/bin/gpg-agent --supervised";
    ExecReload = "${pkgs.gnupg}/bin/gpgconf --reload gpg-agent";
  };
};

This was mainly done to save time since it would require a lot of work to port over the NixOS systemd modules to Home Manager; the systemd configuration in NixOS is extensive and deeply tied into many parts of NixOS. It should, in an perfect world, be possible to adjust the systemd module in Nixpkgs such that it can be used by Home Manager as well.

X session

This is a tricky area but Home Manager is happy to manage your X session and ensure that graphical services like Redshift run as expected when you do a graphical login. The overall goal was to use systemd user sessions as much as possible, in particular Home Manager uses graphical-session.target and graphical-session-pre.target to ensure that graphical services are started and stopped together with the graphical session. This way of starting graphical services is relatively new and untested compared to simply running them inside the .xsession script but it does bring the same benefits as running system services through systemd.

Unfortunately we cannot at the moment go full out on the systemd-ization of the graphical session by also making the window manager itself a service. This is because the --wait option of systemctl start only appears to work when the user D-Bus is socket activated through systemd. While NixOS does support socket activated user D-Bus through the services.dbus.socketActivated option it isn’t enabled by default, so to avoid making this option a requirement we simply treat the window manager command in a special way and run it directly from the generated ~/.xsession script.

In practice this all means that you can have something pretty minimal in the NixOS configuration such as

services.xserver.enable = true;

and in the Home Manager configuration something like

xsession = {
  enable = true;
  windowManager =
    let
      xmonad = pkgs.xmonad-with-packages.override {
        packages = self: [ self.xmonad-contrib ];
      };
    in
      "${xmonad}/bin/xmonad";
  initExtra = ''
    # Turn off beeps.
    xset -b
  '';
};

services.redshift = {
  enable = true;
  latitude = "55.704";
  longitude = "13.195";
};

This would create an ~/.xsession script roughly like

. "$HOME/.profile"

# If there are any running services from a previous session.
systemctl --user stop graphical-session.target
systemctl --user stop graphical-session-pre.target

systemctl --user start hm-graphical-session.target

# Turn off beeps.
xset -b

/nix/store/qan8zxnj1h7l5lk24i0bzpbdjk9s4n3h-xmonad-with-packages/bin/xmonad

systemctl --user stop graphical-session.target
systemctl --user stop graphical-session-pre.target

and the generated Redshift service file is

[Install]
WantedBy=graphical-session.target

[Service]
ExecStart=/nix/store/zr…r4-redshift-1.11/bin/redshift -l 55.704:13.195 -t 5500:3700 -b 1:1
Restart=always
RestartSec=3

[Unit]
After=graphical-session-pre.target
Description=Redshift colour temperature adjuster
PartOf=graphical-session.target

Final thoughts

Overall I’m reasonably pleased with Home Manager and the convenience it brings in managing your $HOME across several computers. It certainly still has some issues that need to be solved, like making rollbacks more accessible and developing more modules. I also would like a more sophisticated system for reloading user services during activation – ideally to reuse the NixOS system. Finally, it would be really great to have a NixOS module that allows you to configure user environments directly from the system configuration file, I would especially like this for containers.

In any case, despite these shortcomings I think Home Manager is stable and useful enough for the wider community of people looking for declarative configuration of their home directory. So please give it a try and give feedback!