Last active
October 15, 2025 20:36
-
-
Save udf/4d9301bdc02ab38439fd64fbda06ea43 to your computer and use it in GitHub Desktop.
Revisions
-
udf revised this gist
Nov 26, 2021 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -225,4 +225,6 @@ in And it works! I don't really get exactly why explicitly pulling out the options that we want avoids the infinite recursion. # Conclusion Obviously for this trivial example, putting the merge at the `config.systemd` level makes more sense, but for [a more complex module](https://github.com/udf/nix-hanzo/blob/5b52e5c4fe42ccc36b6007e77e5546026ecb5357/modules/vpn-containers.nix#L49) it definitely helps with readability. Something else to note is that if we wanted to define service options then we would get a recursion error, the solution in that case is to move our module's options to another top level key that we're not going to use in the `config` section (for example, `options.custom.services.zfs-auto-scrub`). -
udf revised this gist
Nov 26, 2021 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -203,7 +203,9 @@ error: infinite recursion encountered What gives? if we take one key from the output of our function and assign it to the module, then it works fine: ```nix { config.systemd = (mkMergeTopLevel (...).systemd); } ``` # Planet status: h4xed -
udf revised this gist
Nov 26, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -163,7 +163,7 @@ lib.attrsets.foldAttrs So all we need to do is wrap `foldAttrs` in a `mapAttrs` so we can put each list through `mkMerge`: ```nix { mkMergeTopLevel = attrs: ( mapAttrs (k: v: mkMerge v) (foldAttrs (n: a: [n] ++ a) [] attrs) ); } -
udf revised this gist
Nov 26, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -223,4 +223,4 @@ in And it works! I don't really get exactly why explicitly pulling out the options that we want avoids the infinite recursion. # Conclusion Obviously for this trivial example, putting the merge at the `config.systemd` level makes more sense, but for [a more complex module](https://github.com/udf/nix-hanzo/blob/5b52e5c4fe42ccc36b6007e77e5546026ecb5357/modules/vpn-containers.nix#L49) it definitely helps with readability. -
udf created this gist
Nov 26, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,226 @@ # The Setup I wanted to write a module that generates multiple systemd services and timers to scrub some zfs pools at certain intervals. The [default scrub config](https://github.com/NixOS/nixpkgs/blob/fc9ee3bc0b9e6d30f76f4b37084711ae03114e21/nixos/modules/tasks/filesystems/zfs.nix#L304) does not support individual scrub intervals for each pool. I want the config to look like this: ```nix { services.zfs-auto-scrub = { tank = "Sat *-*-* 00:00:00"; backups = "Sat *-*-* 06:00:00"; }; } ``` So let's define the basic structure of our module: ```nix { config, lib, pkgs, ... }: with lib; let cfg = config.services.zfs-auto-scrub; in { options.services.zfs-auto-scrub = mkOption { description = "Set of pools to scrub, to when to scrub them"; type = types.attrsOf types.str; default = {}; }; confg = {}; # TODO: implement } ``` Side note: I don't bother with an enable option for my own modules, because I comment out the import in my main config to disable a module, but feel free to add it if you're following along. So far pretty normal, let's use `mapAttrs'` to generate the unit and timer for each pool: ```nix { # ... config.systemd.services = ( mapAttrs' (name: interval: nameValuePair "zfs-scrub-${name}" { description = "ZFS scrub for pool ${name}"; after = [ "zfs-import.target" ]; serviceConfig = { Type = "oneshot"; }; script = "${config.boot.zfs.package}/bin/zpool scrub ${name}"; }) cfg ); config.systemd.timers = ( mapAttrs' (name: interval: nameValuePair "zfs-scrub-${name}" { wantedBy = [ "timers.target" ]; after = [ "multi-user.target" ]; timerConfig = { OnCalendar = interval; Persistent = "yes"; }; }) cfg ); } ```` Well, that's not so bad for this simple example, but I'm sure you can see how repetitive it gets to have to `mapAttrs'` for every key that you want to generate. # Merge all the keys! Enter [mkMerge](https://nixos.org/manual/nixos/stable/#sec-option-definitions-merging), it takes a list of options definitions and merges them. So we should be able to generate the units and timers individually and merge them into one at the top, right? ```nix { # ... config = mkMerge (mapAttrsToList ( name: interval: { systemd.services."zfs-scrub-${name}" = { description = "ZFS scrub for pool ${name}"; after = [ "zfs-import.target" ]; serviceConfig = { Type = "oneshot"; }; script = "${config.boot.zfs.package}/bin/zpool scrub ${name}"; }; systemd.timers."zfs-scrub-${name}" = { wantedBy = [ "timers.target" ]; after = [ "multi-user.target" ]; timerConfig = { OnCalendar = interval; Persistent = "yes"; }; }; } ) cfg); } ``` Right? ``` building Nix... error: infinite recursion encountered, at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:131:21 (use '--show-trace' to show detailed location information) ``` Guess not. # jk... unless 😳 There is [a quick workaround for this case]( https://discourse.nixos.org/t/infinite-recursion-in-module-with-mkmerge/10989/6), since we're only generating `systemd.*` options we can put our merge at the `config.systemd` level: ```nix { # ... config.systemd = mkMerge (mapAttrsToList ( name: interval: { services."zfs-scrub-${name}" = { # ... }; timers."zfs-scrub-${name}" = { # ... }; } ) cfg); } ``` (repeated options omitted for brevity) While this works, it's really only fine for simple modules that generate options under one top level key. For example let's say we wanted to generate some users as well (doesn't fit with the example, but bare with me). If we add another option like `config.users = mkMerge ...`? Then we're back to square one. # Hack the planet What if we were to put the `mkMerge`'s one level lower? Essentially we would want to turn a list of options like: ```nix [ { a = 1; } { a = 2; b = 3; } ] ``` into ```nix { a = mkMerge [ 1 2 ]; b = mkMerge [ 3 ]; } ``` (imagine the integers as real options). It seems like a complicated problem, but there's a function in lib that solves the whole thing for us, [foldAttrs](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.attrsets.foldAttrs). The example looks exactly like our problem! ```nix lib.attrsets.foldAttrs (n: a: [n] ++ a) [] [ { a = 2; b = 7; } { a = 3; } { b = 6; } ] => { a = [ 2 3 ]; b = [ 7 6 ]; } ``` So all we need to do is wrap `foldAttrs` in a `mapAttrs` so we can put each list through `mkMerge`: ```nix { mkMergeTopLevel = names: attrs: ( mapAttrs (k: v: mkMerge v) (foldAttrs (n: a: [n] ++ a) [] attrs) ); } ``` Let's slap that into our module: ```nix let # ... mkMergeTopLevel = attrs: ( mapAttrs (k: v: mkMerge v) (foldAttrs (n: a: [n] ++ a) [] attrs) ); in { # ... config = mkMergeTopLevel (mapAttrsToList ( name: interval: { systemd.services."zfs-scrub-${name}" = { # ... }; systemd.timers."zfs-scrub-${name}" = { # ... }; } ) cfg); } ``` And... ``` building Nix... error: infinite recursion encountered ``` (i cri) What gives? if we take one key from the output of our function and assign it to the module, then it works fine: ```nix config.systemd = (mkMergeTopLevel (...).systemd); ``` # Planet status: h4xed So clearly what we're doing is legal, so lets explicitly pull out the option(s) that we want using `getAttrs`: ```nix let mkMergeTopLevel = names: attrs: getAttrs names ( mapAttrs (k: v: mkMerge v) (foldAttrs (n: a: [n] ++ a) [] attrs) ); in { # ... config = mkMergeTopLevel ["systemd"] (...); } ``` And it works! I don't really get exactly why explicitly pulling out the options that we want avoids the infinite recursion. # Conclusion Obviously for this trivial example, putting the merge at the `config.systemd` level makes more sense, but for a more complex module