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).
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"
];
}
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.
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)
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?
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.
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.
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.
@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:
Document this behaviour better in the nix-maid docs.
Write an optional systemd service that cleans dead symlinks into the nix-maid static dir.
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.
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.