Home Manager is a great part of the Nix ecosystem, allowing you to easily manage your home directories and configurations. I use Home Manager with Nix flakes, and am quite happy, except for one very annoying aspect of it: since I use Home Manager as a NixOS module, anytime I make a change to a program’s configuration file, I need to rebuild the entire system in order for those changes to take effect. This slows down many workflows that involve updating files in home directories (mainly program configurations).
We’ll use the example of neovim. Home Manager takes my configuration
options (the options I set for neovim through the home manager module,
and also the path to the raw lua/vim configuration files that I point
to) and builds them into a full neovim configuration (setting the
correct runtime paths, putting plugins in the right places, and so on).
It then puts that configuration in an entry in this nix store, and
symlinks it to the appropriate home directory
(~/.config/nvim
in the case of neovim). All of this does
give us the benefits of nix, i.e reproducibility, and having our neovim
configuration in the same place as the rest of our system, neatly
organized inside the flake.
The problem is that home manager is part of my system’s NixOS configuration. Let’s take a look at (a shortened version of) my flake:
{
description = "some description";
inputs = {
# ... nixpkgs, other inputs...
home-manager = {
url = "github:nix-community/home-manager/release-24.05";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, ... }: {
nixosConfigurations = {
HOSTNAME = nixpkgs.lib.nixosSystem {
modules = [
./host/HOSTNAME
.nixosModules.home-manager
hm
{
home-manager = {
# ... HM config ...
};
}
];
};
};
# ... other outputs ...
};
}
Since the HM configuration is part of the nixos configuration, any changes made need a rebuild of the entire system:
$ nixos-rebuild switch --flake path/to/flake#HOSTNAME
Although nix is smart and only rebuilds the parts that are necessary, it is nonetheless a much slower process than it needs to be.
1 The easy solution
The easiest solution is to use mkOutOfStoreSymlink
:
# in a home manager module
.neovim = { enable = true; };
programs
.configFile."nvim" = {
xdgenable = true;
recursive = true;
source = config.lib.file.mkOutOfStoreSymlink "/absolute/path/to/nvim/config";
}
This takes in an absolute path to your config (which I don’t like very much) and makes a symlink from that (instead of copying it to the nix store and symlinking that). This, too, I don’t like very much, because it doesn’t feel like a very “nix” way of solving things. I will concede that this is the simplest solution, and should cover most cases as well.
2 Taking advantage of flakes
The next two solutions take advantage of flakes to detach packages and their configurations from the entire nixos system configuration (and, consequently, home manager as well). This does mean we won’t have access to all the niceties and options provided by home manager, but in my experience it’s not been a very big loss.
Now, if you’re as unfamiliar with flakes as I was before writing this post, you’ll want to know a bit about flakes and how they work. A lot of people describe flakes as just “a bunch of inputs and outputs”, which is true, but grossly unhelpful. I’ll try to give what is hopefully a more useful answer. Flakes indeed take just about anything as an input - source code, configuration files, or (most commonly) other flakes. It can then produce a number of different types of outputs, with some of the most common being:
- NixOS configurations: you already saw this in the example above. A NixOS configuration tells nix how to build the nixos system;
- Dev shells;
- Packages.
Each of these outputs has a nix CLI command associated with it that allows us to consume that output. For example:
nixos-rebuild switch --flake <path/to/flake>#<hostname>
to use a nixos configuration output;nix develop <path/to/flake>#<devshell>
to enter a dev shell;nix run <path/to/flake>#<pkg-name>
to build and run a package.
We’re interested in that last one. We’ll take the original package,
somehow bundle our configuration together with it, and then use
nix run <flake>#<pkg>
to build just that
package (along with our configuration) and run it. This cleanly avoids
having to rebuild the system, while still making sure our configuration
is handled by nix.
2.1 Overriding the package
We can modify nix packages by overriding the arguments passed to them
or their attributes, using <pkg>.override
or
<pkg>.overrideAttrs
respectively. A lot of packages
will take extra configuration as an argument, and we can override them.
Sometimes this is enough to achieve what we want. Neovim is one such
package, so we’ll try it out.
Let’s start by adding an output to the flake:
# In flake.nix...
{
# ... Inputs, description, etc...
outputs = { nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
# ... Rest of the outputs...
packages.${system}.neovim = pkgs.callPackage ./pkgs/neovim { };
}
}
pkgs.callPackage
will call the function in
pkgs/neovim/default.nix
, and pass all of its attributes as
arguments.
Next, we create the directory pkgs/neovim
which will
contain a default.nix
and another directory that contains
our entire neovim configuration, like so:
pkgs/neovim/
├── default.nix
└── dots/
├── ftplugin
├── lua └── README.md
Inside default.nix
, we will take in as arguments the
neovim
package, and vimUtils
(which we will
use to turn our own configuration into a “plugin”):
{ neovim, vimUtils }:
let
# My Neovim Config, as a plugin.
config-nvim = vimUtils.buildVimPlugin {
name = "config-nvim";
src = ./dots;
};
# Neovim package with all the plugins and config.
in neovim.override {
withRuby = false;
withPython3 = false;
withNodeJs = false;
configure = {
# Our "customRC" will just call `init.lua`.
customRC = ''lua require("init")'';
packages.foo = {
opt = [];
start = with vimPlugins; [
# My config.
config-nvim
# ... other neovim plugins, e.g lspconfig, telescope, etc...
];
};
};
}
2.2 Wrapping the program
Sometimes overriding the package is not an option.
TODO (if I ever get to it, but the idea is to use the
wrapProgram
function from the makeWrapper
utility, see this
gist).