Nix-Maid: systemd native dotfile management

Hello :wave:

Today I want to present my latest project: nix-maid. It started as a systemd-centric alternative to home-manager, as it only uses tmpfiles and regular user services (as opposed to activation hooks).

Please visit the page for more information, and let me know if you try it: https://viperml.github.io/nix-maid/ (GitHub)

This is an example standalone nix-maid configuration, that can be installed with nix-env -if ./my-config.nix && activate (I also provide a NixOS module and a Flake).

# my-config.nix
let
  sources = import ./npins;
  pkgs = import sources.nixpkgs;
  nix-maid = import sources.nix-maid;
in
nix-maid pkgs {
  # Add packages to install
  packages = [
    pkgs.yazi
    pkgs.bat
    pkgs.eza
  ];

  # Create files in your home directory
  file.home.".gitconfig".text = ''
    [user]
      name=Maid
  '';

  file.xdg_config."zellij/config.kdl".source = ./config.kdl;

  # `file` supports a mustache syntax, for dynamically resolving the value of {{home}}
  # This same configuration is portable between systems with different home dirs
  file.xdg_config."hypr/hyprland.conf".source = "{{home}}/dotfiles/hyprland.conf";

  # Define systemd-user services, like you would on NixOS
  systemd.services.waybar = {
    path = [ pkgs.waybar ];
    script = ''
      exec waybar
    '';
    wantedBy = [ "graphical-session.target" ];
  };

  # Configure gnome with dconf or gsettings
  gsettings.settings = {
    "org.gnome.mutter"."experimental-features" = [
      "scale-monitor-framebuffer" "xwayland-native-scaling"
    ];
  };

  dconf.settings = {
    "/org/gnome/desktop/interface/color-scheme" = "prefer-dark";
  };

  # Mustache syntax is also available in dynamicRules
  systemd.tmpfiles.dynamicRules = [
    "L {{xdg_config_dir}}/hypr/workspaces.conf - - - - {{home}}/dotfiles/workspaces.conf"
  ];
}
40 Likes

Why use Moustache when one already has access to Nix for (IMO) much stronger templating support?

2 Likes

I think it’s for allowing runtime resolution as opposed to eval/build time. I don’t think you could meet these two goals without doing runtime resolution.

🌐 Portable: Defers the value of your home directory, so the same configuration works for different users.
đŸš« No Legacy: API redesigned from scratch, avoiding past mistakes like mkOutOfStoreSymlink.
1 Like

The mustache syntax is for values that are resolved at runtime, that’s a feature I wanted to use myself.

1 Like

OH, this is perfect !
Is exactly what i’m looking, i don’t really like Home Manager and this look a much simpler solution.
Thank You !

2 Likes

Interesting project!

The website says:

API redesigned from scratch, freeing us from past mistakes like mkOutOfStoreSymlink

What does this mean exactly? Can Nix-Maid create simple symlinks like ~/.foo -> ~/.dotfiles/foo without hackery (i.e. does it avoid intermediate symlinks through the Nix store etc.)?

HM breaks assumptions from NixOS, e.g. environment.etc."foo".source = "/bar" is fine on NixOS, but in HM it will try to cast the string into a path, which later fails when using flakes because of the eval sandbox. mkOutOfStoreSymlink exists to restore that functionality, and to me it’s a hack more than anything. For that reason, .source in nix-maid works the same as in NixOS.

The other topic is that nix-maid allows you to use a moustache syntax for runtime variables, so you can do either file.home.".foo".source = "/home/myuser/.dotfiles/foo or file.home.".foo".source = "{{home}}/.dotfiles/foo (both without mkOutOfStoreSymlink when using a string literal)

4 Likes

helloooo! :smiley_cat:

the time has finally come for me to declare all of my dotfiles


there are home-manager and nix-maid.

with home-manager i quickly found that it does not support EVERY single option there is for EVERY single package, for example, for pkgs.micro there is a programs.micro.settings, but no programs.micro.bindings. this would imply that i will have to resort to xdg.configFile."micro/bindings.json".text or home.file.<path/name>.text, which is a symlink


but with nix-maid
 there are no “options” in the usual sense. there are ONLY symlinks, apparently? i tried the same, but with nix-maid: file.xdg_config."micro/bindings.json".text - and it worked perfectly. but
 it got me thinking. what would programs.micro.bindings option have done (if it existed), in comparison to the maid.*.text option? obviously, one is a symlink. but
 is that a bad thing? and is it possible not to create a symlink, but just A file with text?

thanks in advance, im still learning!!! :smile_cat:

Most home-manager modules are largely just ways to create the text of files which are then installed through home.file. You can absolutely just not use that and directly write the text of the config file
 and personally, I’ve found that I don’t honestly want to use many of the module from home-manager. The key functionality is the ability to manage the files.

1 Like

One thing you need to make clear in the introduction is whether nix-maid cleans up after itself. Generally, systems based on systemd’s tmpfiles.d don’t do that. If you remove the declaration of a symlink, it sticks around on the filesystem until you manually delete it. This is the biggest thing that keeps me using home-manager’s home.file despite everything aggravating about it.

3 Likes

hmm. is that a big deal? cos yeah, it doesnt
 :confused:

i mean, isnt there like a command or another helper thingie that removes orphaned symlinks? cos i mightve made a mess just now lol. actually, it doesnt even appear to be an orphan symlink at all! even though i have un-declared it, it’s still out there, in the nix/store/
 oh, yeah, thats right, doesnt it EVENTUALLY go away in a few generations, no?

i was kinda hoping that boot.tmp.cleanOnBoot would sort it out, but no


There are certainly ways to remove orphaned symlinks, but that’s not really “correct” as a solution to this problem, since the file still being in the nix store doesn’t mean it’s part of the current generation of your config
 as you apparently just discovered, given your edit.

1 Like

@tejing Regarding the removal of old symlinks, nix-maid uses the static directory method that NixOS uses (but with another layer of indirection as the contents of the static directory are generated at runtime).

With this method, instead of creating direct symlinks ~/.gitconfig -> /nix/store/...-gitconfig, you put a directory in between:

For the first generation, following the chain ~/.gitconfig resolves to the actual gitconfig in the nix store. To perform an upgrade, instead of touch ~/.gitconfig we can just change which generation the static directory points to.

When we upgrade to a generation (3rd case) where .gitconfig is no longer configured by the user, ~/.gitconfig becomes a stale link into the new generation. Therefore, the configuration has been “removed” without having to remove the old symlinks.

This works fine under the assumptions that:

  • The program reading the config file properly handles dead symlinks.
  • The program reading the config file doesn’t get upset for 2 more layers of symlink indirection.


which for the programs I use, they are true.

For the future I plan to:

  1. Document this behaviour better in the nix-maid docs.
  2. Write an optional systemd service that cleans dead symlinks into the nix-maid static dir.
  3. Consider moving away from systemd-tmpfiles. We can probably do the same with a tiny Rust/Go program that ingests a manifest similar to tmpfiles, but with better logging etc and compatible with macos.
4 Likes

(I thought this was already in nix-maid, but in some of the rewrites I forgot to add the static linking behaviour, it should be fine now: Fix static linking · viperML/nix-maid@534317b · GitHub)

2 Likes

Thanks for the detailed explanation. Leaving them as dead symlinks is certainly an improvement on leaving them as working ones. And I’ve always thought it was strange that home-manager doesn’t use the static directory idea that nixos uses for /etc. Both atomicity and the simplicity of keeping track of everything push for it.

Idea: You could probably improve the performance of the “cleaning” process (when/if you implement it) by walking the old static directory rather than the entire home directory. Anything you’re supposed to clean should be in that. Nixos’ etc script doesn’t do it, but there usually isn’t much in etc that isn’t declaratively managed, anyway, so the savings would be minimal there. That would require the cleaning process to be part of the activation, though, not a separate service.

2 Likes