Unified Modules For Your Nixfiles
7 points by knl
7 points by knl
I ended up following the same pattern an year ago :) https://github.com/wantguns/dotfiles
i was able to expose an interface/attrset for each of my host to opt in to my wireguard mesh based on the common nix state, which was fun
I've also been putting HM and OS modules in the same file and am really pleased with it. Haven't had a reason to change in 5+ years, and happy to see others discovering the same!
I think in the open source world I've first seen it implemented by flake-aspects & den.
Inspired by languages with opinionated module systems, I'm enforcing each module's file path in the repo is the same as in the config (actually it's automated via lib.setAttrByPath). So modules/a/b.nix is always config.a.b.*.
That's a small constraint that's both freeing and allows automating more boilerplate setup: I can automatically get the classic cfg set to the current module's option values!
Simplifying a bit, the module exposing options.x.y is has an extra cfg arg that's automatically set to config.x.y.
Know When To Stop
Over time I've started to actually default to shared modules, just without options.
It feels a bit weird to commit code in a reusable module that actually has host specific values hardcoded, but doing it means that when I actually need to reuse I'm a step closer already and the path of least resistance is just to add a couple options, not copy-pasting, avoiding future headaches. Updating the host already using the module to use a couple new options is trivial.
Do you have a link to your setup? Or something showing that use of setAttrByPath? It sounds intriguing :)
It's private unfortunately, I'm not comfortable sharing the whole thing publicly because it has a lot of identifying info, and details about my personal infra I'd rather keep separate from this public identity.
It essentially works like:
let
path = "relative/to/modules/root.nix";
attrPath = path |> lib.split "/";
in
{
options = lib.setAttrByPath attrPath {
x = lib.mkOption { ... };
};
# Elsewhere:
config = {
relative.to.modules.root.x = 1;
};
}
Using the same attrPath you can do cfg = lib.attrByPath attrPath config instead of the manual cfg = config.relative.to.modules.root.
The harder part is to wrap this in a nice system so you don't have the same boilerplate in each file. I use a function that processes each file's content before the module system sees it. So my files aren't actually modules like they would be in flake-parts, but args to that custom function.
I'm not 100% happy with that pattern, but I haven't been able to get cfg to work nicely using modules à la flake-parts.
In practice the files look like:
{ config, lib, ... }: # optional args like normal modules
{
description = "mandatory short doc";
options = { # optional
# `enable` is defined by default, even if `options` is not in these attrs
};
homeManager = cfg: { # optional, returns standard config values, gated behind `mkIf cfg.enable` automatically
# cfg being `lib.attrByPath attrPath config`
};
nixos = cfg: { }; # same pattern as above but for NixOS
}
The "mkModule" function is called once to generate the HM module, and once for the NixOS one. That's actually just done by the module system: the function returns a module that is imported in both configs. The function uses the _class special module argument to know which of those evaluations it currently is in, and thus which config to generate.