Host Specific Configs

I have been using Home Manager to manage my dotfiles on my Pop! OS laptop for about seven months now. I highly recommend it over other dotfile schemes. In the past 3 months changed my server from Windows to Pop! OS as well.

I was impressed with the ability to scp my dotfiles over and, after installing Nix , have my command-line tools at my disposal in 2 minutes flat. But, installing dependencies I did not want on that machine bothered me. I don’t need flutter on my headless server. This weekend I have decided to tackle this problem and I thinking came up with a pretty good solution that I would like to share!

I won’t go deep on Home Manager itself, here are some resources for that:

  1. https://seroperson.me/2024/01/16/managing-dotfiles-with-nix/
  2. https://juliu.is/tidying-your-home-with-nix/

The Basics

If you are here, I expect you to have a flake.nix, a home.nix and are now looking to add a new host configuration. To start:

  1. Change homeConfigurations."{user}" to homeConfigurations."{user}@{host}"
  2. Move home.nix into a new directory ./home/{host}.nix
Path: ~/.dotty/flake.nix
 1{
 2  description = "Home Manager configuration of neal";
 3
 4  inputs = {
 5    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
 6    home-manager = {
 7      url = "github:nix-community/home-manager";
 8      inputs.nixpkgs.follows = "nixpkgs";
 9    };
10  };
11
12  outputs = { nixpkgs, home-manager, ... }:
13    let
14      system = "x86_64-linux";
15      pkgs = nixpkgs.legacyPackages.${system};
16    in
17    {
18      homeConfigurations."neal@devone" = home-manager.lib.homeManagerConfiguration {
19        inherit pkgs;
20        modules = [ ./home/devone.nix ];
21        extraSpecialArgs = {
22          username = "neal";
23          homeDirectory = "/home/neal";
24        };
25      };
26    };
27}

Defining username and homeDirectory in extraSpecialArgs will make sense later. Just know, those are now being passed to your {host}.nix file as arguments

Now to add a new host you can add a new homeConfigurations and copy your existing ./home/{host}.nix as ./home/{new_host}.nix

Path: ~/.dotty/flake.nix
 1{
 2  ...
 3
 4  outputs = { nixpkgs, home-manager, ... }:
 5    let
 6      system = "x86_64-linux";
 7      pkgs = nixpkgs.legacyPackages.${system};
 8    in
 9    {
10      homeConfigurations."neal@devone" = home-manager.lib.homeManagerConfiguration {
11        ...
12      }
13      homeConfigurations."neal@server" = home-manager.lib.homeManagerConfiguration {
14        inherit pkgs;
15        modules = [ ./home/server.nix ];
16        extraSpecialArgs = {
17          username = "neal";
18          homeDirectory = "/home/neal";
19        };
20      }
21}

You now have two distinct Home Manager configurations in a single repository. Yay! 🥳

The Crux

Okay, this is the purpose of this blog post. Why in gods name would I want to distinct Home Manager configurations? The entire purpose of dotfiles is to have easy transitions between machines and having to edit two distinct files is a quick way to have them diverge. The solution, modules and using Home Manager’s imports feature.

Now this is pretty common idea. From the dotfiles I have looked at the common scheme is breaking up programs into their own file with their configuration. With that scheme I would have a single file fish.nix that installs fish and configures it. This doesn’t necessarily work with a non-NixOS install as my system has some packages installed with apt or flatpak that need to be added to the PATH. My solution to this is broader modules, here is a snippet from my ./modules/shell.nix

Path: ~/.dotty/modules/shell.nix
 1{ pkgs, homeDirectory, ... }:
 2{
 3  home.packages = [
 4    pkgs.fish
 5
 6    pkgs.starship
 7    pkgs.eza
 8    pkgs.fd
 9    pkgs.zoxide
10    pkgs.ripgrep
11  ];
12
13  home.sessionVariables = {
14    EMAIL = "neal@joslin.io";
15  }
16
17  home.shell.enableFishIntegration = true;
18
19  programs.fish = {
20    enable = true;
21    interactiveShellInit = ''
22      set fish_greeting
23    '';
24    shellInit = /* fish */ ''
25      fish_vi_key_bindings
26      fish_add_path --global ${homeDirectory}/.local/bin
27    '';
28    shellAliases = {
29      ls = "eza --icons";
30    }
31  }
32
33  programs.bash = {
34    enable = true;
35    initExtra = ''
36      if [[ $(${pkgs.procps}/bin/ps --no-header --pid=$PPID --format=comm) != "fish" && -z ''${BASH_EXECUTION_STRING} ]]
37        then
38          shopt -q login_shell && LOGIN_OPTION='--login' || LOGIN_OPTION=""
39          exec ${pkgs.fish}/bin/fish $LOGIN_OPTION
40          fi
41    '';
42  };
43
44  programs.zoxide.enable = true;
45}

This is how you would use this file inside of a ./home/{host}.nix

Path: ~/.dotty/home/devone.nix
 1{ config, pkgs, username, homeDirectory, ... }:
 2{
 3  imports = [
 4    ../modules/shell.nix
 5  ];
 6
 7  home = {
 8    inherit username homeDirectory;
 9    stateVersion = "24.05";
10  };
11
12  programs.home-manager.enable = true;
13}

This shows the reason for defining username and homeDirectory inside of flake.nix. They are automatically passed to all imports!

Cool… Modules?

So far this is nice and we can breakup the configuration, but if we have some flatpak package that needs to be added to the PATH in fish everything falls apart because it overwrites the existing shellInit in shell.nix, right?- NO, instead it adds both of them.

Path: ~/.dotty/home/devone.nix
 1{ config, pkgs, username, homeDirectory, ... }:
 2{
 3  imports = [
 4    ../modules/shell.nix
 5  ];
 6
 7  home = {
 8    inherit username homeDirectory;
 9    stateVersion = "24.05";
10  };
11
12  programs.fish.shell.shellInit = /* fish */ ''
13    fish_add_path --global ${homeDirectory}/Android/Sdk/emulator
14    fish_add_path --global ${homeDirectory}/Android/Sdk/platform-tools
15  '';
16
17programs.home-manager.enable = true;
18}

Now this outputs a fish configuration file that contains:

Path: ~/.config/fish/config.fish
1
2
3
4
5
set fish_greeting
fish_add_path --global /home/neal/.local/bin

fish_add_path --global /home/neal/Android/Sdk/emulator
fish_add_path --global /home/neal/Android/Sdk/platform-tools

I’m unsure why I was surprised about this. I understood doing this with dictionary settings, like shellAliases, merge the dictionary. The same going for string settings is cool and opens up a lot of opportunities.

Expanding

The next step of this idea is away from host specific apt/flatpak programs and instead module specific. A prime example of this is my development.nix file. Here is a trimmed down example:

Path: ~/.dotty/modules/development.nix
 1{ pkgs, homeDirectory, ... }:
 2{
 3  home.packages = [
 4    pkgs.python311Full
 5    pkgs.python311Packages.pipx
 6    pkgs.nodejs_20
 7
 8    pkgs.ruff
 9    pkgs.typos
10    pkgs.markdownlint-cli
11  ];
12
13  home.sessionVariables = {
14    WORKON_HOME = "${homeDirectory}/.virtualenvs";
15    VIRTUAL_ENV_DISABLE_PROMPT = 1;
16  };
17
18  home.file = {
19    ".textlintrc.json".source = ../config/textlintrc.json;
20    ".local/scripts/work".source = ../scripts/work;
21  };
22
23  programs.fish = {
24    shellInit = /* fish */ ''
25      fish_add_path --global ${homeDirectory}/.dotty/node_modules/.bin
26      fish_add_path --global ${homeDirectory}/.local/scripts/work
27    '';
28    shellAliases = {
29      pyunit = "python -m unittest discover";
30      pycoverage = "coverage run -m unittest discover";
31      pyreport = "coverage report -m";
32      pysecret = "python -c 'import secrets; print(secrets.token_hex())'";
33      djrun = "python manage.py runserver";
34      djshell = "python manage.py shell";
35      djmigrations = "python manage.py makemigrations";
36      djmigrate = "python manage.py migrate";
37    };
38  };
39
40  programs.ruff = {
41    enable = true;
42    settings = {
43      line-length = 80;
44      target-version = "py311";
45      format = {
46        quote-style = "double";
47      };
48    };
49  };
50}

After adding this module I have access to my common development aliases and work scripts. I have a mail.nix module that follows the same scheme, after I add that module I have access to all my neomutt aliases and email sending scripts. No host gets extra aliases, PATHs or scripts it doesn’t need.

Conclusion

Wrapping up, If you are looking to add multi-host support to your Home Manager dotfiles give this scheme a shot. Don’t try to cram all settings for a program into a single module when it doesn’t make sense. This scheme is broad while still being flexible and is probably the most efficient way to organize.

Series:
home-manager
Post:
0/0
comments powered by Disqus