Managing config files: why not use mkOutOfStoreSymlink for everything?

I recently discovered mkOutOfStoreSymlink while tweaking my Waybar config, and it’s been a game changer - no more waiting for home-manager switch after every small change.

This got me thinking: if symlinking config files directly from my dotfiles repo works so well, why should I ever use Nix options (like programs.waybar.settings) to generate config files?

The only reasons I can think of:

  • Need dynamic config based on hostname or other Nix variables
  • Want everything purely declarative in Nix
  • Need to reference pkgs paths in config

But for most config files (JSON/YAML/TOML/CSS):

  • They’re mostly static
  • They get tweaked frequently (especially during UI tuning)
  • Git can version them just fine
  • Nix still handles environment/dependencies perfectly

My current approach:

xdg.configFile."waybar".source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/waybar";

Questions for the community:

  1. What scenarios genuinely require (or are better served by) Nix-generated configs?

  2. Are there any hidden pitfalls to using mkOutOfStoreSymlink everywhere?

  3. Is “Nix for environment, Git for dotfiles” considered a recommended practice?

Thanks for your insights!

7 Likes

It’s up to your workflow. I don’t find directly linking an entire dir very useful personally, writing out configs by hand is soooo 2003.

And of course, if we start needing to refer to other store paths, then it’s useless to write the config by hand.

One problem with mkOutOfStoreSymlink that I’ve faced recently is when you are rebuilding remote machines, i.e.:

  1. have a flake stored in machine A, such as your notebook
  2. configure a remote machine B in that flake
  3. rebuild B from A
  4. nixos-rebuild boot --flake .#B --target-host B --use-remote-sudo --ask-password

In this case, even though you rebuilt it, dotfiles will not be present or will not be updated.
I’ve found myself having to rsync the flake and it’s dotfiles due to this.

Despite that, I still mkOutOfStoreSymlink my neovim configuration exactly because of the dynamic and quick iteration nature of it’s config. The rest is left to nix.

2 Likes

You can always have custom profiles to point to things, bunching together with as much granularity as needed.

As others have pointed out, there are of course downsides to mkOutOfStoreSymlink. I still really like it, though. The main places I don’t use it are ~/.bashrc or ~/.zshrc which I think are better managed by Home Manager.

One additional benefit of mkOutOfStoreSymlink that I don’t recall seeing mentioned is that sometimes I’ll make a config change to a tool without realizing it (e.g. switching VS Code theme via the GUI). Then later when I open up my personal config flake Git repo for some other reason, I see that the config file is dirty, usually remember why I made the change, and either revert it or commit and push. This bidirectional editing experience is really convenient.

1 Like

It doesn’t allow you to locate settings that a program requires to be in the same file, in different files of your config. Co-locating things that are tightly coupled, regardless of what programs are actually being configured, is one of the biggest advantages of nixos/home-manager in my mind.

It also doesn’t allow you revert a change by rolling back the generation (nor make a change atomically), since the config file’s contents isn’t actually in the generation.

It muddies the separation between config and the deployed generation, meaning remote deployment won’t work, nor will deciding to store your config in a different place. The config has to become an out-of-band part of the deployment.

It complicates future installation, as things don’t work correctly at all without the config put into place, so you need to do that as well during installation (and remember to do so, which is the actually difficult part).

Overall, I feel that using direct links to the config in the generation just breaks far too many abstractions, to the point that I would feel like my config wasn’t reliable if it worked like that. I want declarativity. And I truly don’t mind waiting 30 seconds to see the result of a config change. I don’t really understand how someone ends up with a workflow where that 30 seconds is worth worrying about. In the rare event that I genuinely need a rapid iteration loop (maybe when I’m tweaking an aesthetic setting and I really just need to see how it looks to know what I even want), I just engineer a quick workaround to make the tight loop until I’m happy. Often that means running the program with an explicit config file argument during testing, sometimes it means manually deleting home-manager’s symlink for a bit and letting it get restored when I rebuild later.

Also… a side rant: We shouldn’t need mkOutOfStoreSymlink at all… it should accomplish the same effect to just set source to a string instead of a path-type, like it does for nixos’ environment.etc. What home-manager does with string sources violates the whole point of the distinction between strings and path-types in the nix language. Arg. Anyway. Rant over.

5 Likes

Per rycee that’s an intentional design to discourage people from using it. Clearly it hasn’t worked. It’s easy to underestimate people’s willingness to cargo cult this stuff.

1 Like

The cost of that is that it confuses people on what the difference between strings and path-types even is, though…

1 Like

I can imagine two reasons:

  1. My config is not settled yet, so there are actually a lot of programs whose I’m experimenting with the settings
  2. I’m still new to nix so I can’t simply “engineer a quick workaround” it’s more of a 1h learning exercise (tho of course it’s also good and rewarding)

As a novice, I must say foo/bar being different from "foo/bar" was super weird, especially since it can have a very big performance impact.
But of course making them both do the expensive thing does not help at all …

I really enjoyed your reply however, it really allowed me to understand your point of view, which is a very nice feeling, I hope I managed to do the same for you !

The fundamental purpose of the nix language just involves 2 versions of the filesystem. The filesystem during the nix language evaluation, and the filesystem when the built stuff is getting run (which might not even be on the same machine). You necessarily want to treat these 2 versions differently, and, in particular, when you shift something from the first to the second, you need to copy it to the nix store for the result to deploy properly, as nix antiquotes on path-types automatically do (and unfortunately toString does not do). This is why the distinction exists. Just remember: path-types are eval-time resolved, strings are run-time resolved. And never use toString on a path-type. (Unfortunately it’s sometimes hard to avoid using import on strings, although you should in theory avoid that, too… some of the conventions in that regard are quite unhelpful.)

2 Likes

I haven’t tested it yet, but something which looks like it allows per-machine customization and mutability is yolk.

Another reason I believe is that some modules know about each-other, and so can configure each other accordingly

For example stylix automatically styles kitty, but I don’t think that is possible if you configure kitty outside of nix

These "quick workaround"s have nothing to do with nix, you only need to understand the application you’re configuring, which you will need to anyway when you’re configuring them.

E.g. for emacs it’s typically a question of pressing C-M-x on the expression I want to try out.

It should not really take much more learning than you’re already doing, certainly not an extra hour.

1 Like

I must say I am in general puzzled why home-manager is perceived as essential by the community. My personal setup has always been to install packages globally (such that, when I need to login as root, I have the same environment), to store dotfiles in the same repo as my flake.nix, and to write a tiny script myself to link stuff up. I wish that something like mkOutOfStoreSymlink was a part of stock nixpkgs, to remove the need for a script without adding extra deps.

It’s essential if you have non-NixOS hosts, at which point you might as well manage all your setups with home-manager for consistency.

I also don’t want everything to be replicated for the root user (let alone other users). Logging in as root is awful practice, sudo -s is your friend if you really need a root shell - if, personally I only ever use the --sudo switch on nixos-rebuild and hardly touch the root user otherwise. Maybe for btrfs quota checks or snapshots; everything else is managed appropriately with polkit.

Some more complex applications like e.g. Firefox or gtk dbus shenanigans are also nowhere near trivial enough for me to want to maintain my own script, I’m happy to farm that off to the community.

Though, to be fair, the home-manager module quality is low enough that I’m planning to move away from it eventually. This is also true for a large number of NixOS modules.

But if anything once I get around to it I’ll switch to managing my dotfiles with specific wrappers and users.users.<me>.programs, plus probably hjem for non-NixOS systems.

3 Likes

If you think of it in terms of “dotfiles”, then yeah, home-manager is kinda superfluous. The ability to specify those settings in the nix language, specifically the module system, is the value proposition. I don’t have “dotfiles”. I have a bunch of modules defining options that ultimately create my dotfiles. Not the same thing.

It doesn’t exist in nixpkgs because there’s absolutely no need for it. It’s only necessary in home-manager because home-manager mishandles the string/path distinction. In nixos, you can just use a string instead of a path in places like environment.etc or the tmpfiles rules, and you get things linked out of store without going in circles first.

4 Likes

I believe that’s the special case. It does work for /etc, but doesn’t work for /home/matklad/.config.

NixOS doesn’t have an option to write there directly, you had to mimic that via your own mechanism, or just use a tmpfile rule, which again does properly work with clean path vs string differentiation as environment.etc does.

1 Like

It’s home-manager’s home.file that’s a special case. (And other home-manager options that use the same system, like xdg.configFile)

Most nix code will work the way I’m describing automatically because of how antiquotes work in the nix language. Rycee went to significant lengths to defeat this automatic behavior, for reasons I don’t really understand.

Everywhere that doesn’t involve Rycee doing weird things, you should normally expect that path-types are eval-time resolved (and thus copied to the nix store automatically), and strings are run-time resolved (and thus included verbatim as link targets, if that’s where it ultimately goes).

This is intriguing, I haven’t realized that! I still can’t quite connect the dots here, I must confess.

Let’s say that /home/matklad/dotfiles is my dotfiles/configuration.nix repo, and I want /home/matklad/.config/git/config to be a symlink pointing at /home/matklad/dotfiles/git/config. What should I write in my configuration.nix to achieve that effect, assuming I am not having home manager as a dependency?

Nixos doesn’t really have a good tool for this. It isn’t meant to manage home directories. The closest you’re likely to easily get is tmpfiles.d rules, which will work like I’m describing as normal. They don’t clean up old symlinks when you remove them from the declarative description, though.

3 Likes