The Home Manager Buffet
When I was first introduced to Home Manager, I was initially skeptical of their demo:
# file: "Home Manager snippet configuring git"
programs.git = {
enable = true;
userName = "joe";
userEmail = "[email protected]";
};
# file: "Resulting git configuration"
[user]
name = "joe"
email = "[email protected]"
What if this abstraction of git config is missing an option I rely on? What if HM doesn’t have an equivalent abstraction for all my programs? I also had other flexibility questions regarding writable (not-read-only) files, re-evaluation, etc.
The solution to all these concerns is Home Manager’s “lower-level” (less abstract) functionality that isn’t well advertised in my opinion. As I have adopted HM and used it full time, I have come to realize that Home Manager isn’t a big abstraction you need to commit to, it’s a buffet where you can choose how much or how little abstraction you want on a per-application basis.
What follows is a simplified, commentated buffet menu to help you (and past me) choose what to order.
- Table setting (ground rules)
- Item #1: High level
programs
options - Item #2: Explicit file content written inline
- Item #3: Explicit file content in separate file(s)
- Item #4: Out-of-store symlinks
- Item 5: Not using Home Manager
Table setting (ground rules)
- Regardless of abstraction level, Home Manager can always handle package installation through the
home.packages
option, so I won’t dwell on that much:home.packages = [ pkgs.git ];
- I’m assuming a basic understanding of nix.
- I’m using git configuration as a commonly understood example, but these principles apply to other apps as well.
- If you only know
~/.gitconfig
for user-level git configuration, note that git also supports$XDG_CONFIG_HOME/git/config
, which generally resolves to~/.config/git/config
. I’ll be referencing both to better represent other software.
Item #1: High level programs
options
+ Simple
+ Powerful abstractions
* Uses nix syntax
- Program-specific
- May not have needed controls
- Sometimes renames options from the program-native config
This is the full fat abstraction as shown in the intro. When it’s available and does everything you need, it’s excellent. It will simplify your config by bundling many steps into one, it will update along with your program version, and it will yell when you’ve used an option that doesn’t exist. This approach is likely the entry point you want when configuring new software, but may require a mental switch if you’re transitioning from a program’s native syntax and option naming.
Example
# Create $XDG_CONFIG_HOME/git/config
programs.git = {
enable = true;
userName = "joe";
# ...
};
Notes
- These settings implicitly handle installing the associated package, no
home.packages
needed. - As an example of the powerful abstractions this level provides, configuring git to use delta normally requires installing it, configuring it as your pager, and configuring it for interactive staging; Here it’s a simple one-liner:
programs.git = { # ... delta.enable = true; };
- Keep an eye out for an
extraConfig
key (or similar) in a program’s HM options, as it acts as a nice fallback in case the top level config doesn’t expose everything you need:programs.git = { # ... extraConfig = { merge.conflictStyle = "zdiff3"; # Not a top-level option }; };
Item #2: Explicit file content written inline
+ Universal to all programs
+ Offers program-native control with program-native naming
* Choice of nix or program-native syntax
- Doesn't offer any simplification or abstractions
- No guarantees the output is correct
The home.file
/xdg.configFile
options can be used to generically populate files in the user home or the XDG config home respectively, so should cover almost all dotfile cases not otherwise handled by Home Manager’s high-level options. The text
sub-option serves as a great in-between where you can either provide a content string directly, or you can compute it with other nix functions and attribute sets.
Examples
# Create ~/.gitconfig with content converted from nix attribute set
home.file.".gitconfig".text = lib.generators.toINI {} {
user.name = "joe";
};
# Create $XDG_CONFIG_HOME/git/config in native syntax
home.file."${config.xdg.configHome}/git/config".text = ''
[user]
name = "joe"
'';
# ... or more simply
xdg.configFile."git/config".text = ''
[user]
name = "joe"
'';
# Simpler nix-to-other-syntax example, creating ~/.docker/config.json from nix attribute set
home.file.".docker/config.json".text = builtins.toJSON {
detachKeys = "ctrl-e,e";
};
Notes
- When writing in the “native syntax”, you are just writing in a multiline nix string, so functionality like nix variable substitution is still supported:
home.file.".gitconfig".text = '' [user] name = ${config.home.username} '';
Item #3: Explicit file content in separate file(s)
Building on from item #2:
+ Lets your editor be a better editor
+ Handles directories
* Uses program-native syntax
- Doesn't easily allow for variable substitution
Largely a continuation from the last item, the source
option of home.file
/xdg.configFile
lets you easily use the contents of another file or directory in your dotfiles repo to populate the target location. When embedding your program’s config inside a nix file as shown in item #2, you often lose out on editor features for your program’s native language like LSP support, JSON schema validation, and others; This source
approach resolves that. It is also useful if you have many subfiles for a given program, as with one nix line you can include the whole directory tree.
Examples
# Populate ~/.gitconfig with content of gitconfig.ini (sibling to your nix file)
home.file.".gitconfig".source = ./gitconfig.ini;
# Populate $XDG_CONFIG_HOME/git/config with content of gitconfig.ini (sibling to your nix file)
xdg.configFile."git/config".source = ./gitconfig.ini;
# Populate $XDG_CONFIG_HOME/git/ with the files inside sibling directory git-config-dir
xdg.configFile."git".source = ./git-config-dir;
Notes
- The source files are copied to the nix store with all the store’s regular hashing goodness. This does mean, however, that the resulting output is still readonly and requires a HM switch to update like all the previous approaches.
- You can use
builtins.readFile
as a sort of hybrid between this approach and the last, wherereadFile
returns the string contents of a separate file to then be further manipulated:home.file.".gitconfig".text = builtins.readFile ./gitconfig.ini + "# appending config with this";
Item #4: Out-of-store symlinks
+ Output maintains permissions of input file (i.e. not read only)
+ Changes don't require Home Manager re-evaluation (switch)
+ Handles directories
* Uses program-native syntax
- No reproducibility guarantees
For when you just need a good ol’ symlink from your dotfiles repo to a target location, à la GNU stow. A simple link back to your writable repo file makes this is a great option for config that changes very frequently, or for config you want to be writable from a program’s settings manager (like git config set ...
in this case). The downside of bypassing the nix store is it can no longer make you any guarantees, particularly file existence guarantees; expect silently-created broken links if you rename a file without updating the symlink nix-side.
Examples
# Symlink ~/.gitconfig to ~/dotfile-repo/gitconfig.ini
home.file.".gitconfig".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dotfile-repo/gitconfig.ini";
# Restructured with let binding to help improve readability
home.file = let
mkOutOfStoreSymlink = config.lib.file.mkOutOfStoreSymlink;
repoDir = "${config.home.homeDirectory}/dotfile-repo";
in {
".gitconfig".source = mkOutOfStoreSymlink "${repoDir}/gitconfig.ini";
};
Notes
- If you investigate the resulting symlink (
ls -l ~/.gitconfig
), you may be confused to see it still going to the nix store. That is expected, a chain of symlinks is actually created, which eventually lead to your target file. - The use of an absolute path in a string (rather than a relative, path-type path) is intentional and needed when using nix flakes. The use of variables in the path string helps, but the examples shown above could be improved by using an explicit dotfiles repo location option like shown here.
Item 5: Not using Home Manager
Bit of a cop-out, but I thought it was worth mentioning that Home Manager won’t suddenly take over your system or all your dotfiles without warning. You can absolutely use it for some specific apps and fall back to another dotfile system for the rest. HM checks that it isn’t overwriting existing files during evaluation, and nix’s module import system lets HM co-exist in a dotfiles repo.