Skip to content

Instantly share code, notes, and snippets.

@jfredett
Last active February 26, 2023 18:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jfredett/e345bd3393e9b52e2c54b485f3d4b580 to your computer and use it in GitHub Desktop.
Save jfredett/e345bd3393e9b52e2c54b485f3d4b580 to your computer and use it in GitHub Desktop.
NixOS Parameterized Module Issue
unpacking channels...
error: infinite recursion encountered
at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:256:21:
255| (regularModules ++ [ internalModule ])
256| ({ inherit lib options config specialArgs; } // specialArgs);
| ^
257| in mergeModules prefix (reverseList collected);
(use '--show-trace' to show detailed location information)
building Nix...
error: infinite recursion encountered
at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:256:21:
255| (regularModules ++ [ internalModule ])
256| ({ inherit lib options config specialArgs; } // specialArgs);
| ^
257| in mergeModules prefix (reverseList collected);
(use '--show-trace' to show detailed location information)
building the system configuration...
error: infinite recursion encountered
at /nix/var/nix/profiles/per-user/root/channels/nixos/lib/modules.nix:256:21:
255| (regularModules ++ [ internalModule ])
256| ({ inherit lib options config specialArgs; } // specialArgs);
| ^
257| in mergeModules prefix (reverseList collected);
(use '--show-trace' to show detailed location information)
{ config, lib, pkgs, ... }:
let
ec = (import <nix/common/eclib.nix>);
familyName = "pinky";
in ec.template.mkParamterizedModule {
inherit familyName;
childrenName = "subservices";
parentOptions = {
enable = lib.mkEnableOption "pinky test service";
name = lib.mkOption { type = lib.types.str; default = "${familyName}-family"; };
};
childOptions = {
startScript = lib.mkOption { type = lib.types.str; };
stopScript = lib.mkOption { type = lib.types.str; };
reloadScript = lib.mkOption { type = lib.types.str; default = ""; };
user = lib.mkOption { type = lib.types.str; default = "root"; };
group = lib.mkOption { type = lib.types.str; default = "root"; };
};
mkParent = opts: {}; # not used
mkChild = name: cfg: {
systemd = {
# to be clear, this content doesn't matter, the id function here also causes the issue.
services.${name} = {
enable = true;
description = "Pinky Test Service: ${name}";
after = [ "multi-user.target" ];
wants = [ "${name}.timer" ];
serviceConfig = {
ExecStart = pkgs.writeShellScript "${name}-start.sh" cfg.startScript;
ExecStop = pkgs.writeShellScript "${name}-stop.sh" cfg.stopScript;
ExecReload = lib.mkIf (cfg.reloadScript != "") (pkgs.writeShellScript "${name}-reload.sh" cfg.reloadScript);
Type = "oneshot";
User = cfg.user;
Group = cfg.group;
};
};
timers.${name} = {
enable = true;
description = "Pinky Test Service Timer: ${name}";
after = [ "multi-user.target" ];
partOf = [ "${name}.service" ];
timerConfig.OnCalendar = "*:*:0,15,30,45";
};
};
};
} { inherit config lib pkgs; }
{
mkParameterizedModule = {
familyName,
childrenName,
parentOptions,
childOptions,
mkParent,
mkChild
}:
{ config, pkgs, lib, ...}: with lib; let
parentConfig = mkParent parentConfigOptions;
childrenConfig = mapAttrs mkChild childConfigOptions;
mkChildren = cfg: mapAttrs mkChild cfg;
confs = (parentConfigOptions: childConfigOptions: foldl' (a: v: a // v) (mkParent parentConfigOptions) (mkChildren childConfigOptions));
in {
options.${familyName} = parentOptions // {
${childrenName} = mkOption {
type = types.listOf (types.submodule childOptions);
};
};
config = mkIf parentConfigOptions.enable (confs config.${familyName} config.${familyName}.${childrenName});
# ^^^^^ this and ^^^^ this
# are the offending lines that cause an infinite recursion, but
# I'm confused as to why, since it's normal to refer to options
# like this in a 'regular' module.
};
}
@jfredett
Copy link
Author

Hi! I'm back!

I figured this out -- well, I figured out a bad way to do it that works, and ultimately I think I learned that Nix is not a language that really wants you to encode higher-order things in it. Honestly, the entire exercise has left me thinking that what I'm looking for is a language that 'compiles' (for some value of the word 'compile') to Nix but allows for encoding patterns like this in a nicer way. Someday maybe I'll build that thing I want.

In the meantime, the trick is to just tell Nix to merge the config key one level at a time. Like this:

# TODO: Add some kind of warning if configTree's keys aren't fully contained in supportedKeys
                    # adding support is as easy as copy-pasting the a line below and changing the toplevel key.
                    config.boot = if (hasAttr "boot" configTree) then configTree.boot else {};
                    config.containers = if (hasAttr "containers" configTree) then configTree.containers else {};
                    config.fonts = if (hasAttr "fonts" configTree) then configTree.fonts else {};
                    config.environment = if (hasAttr "environment" configTree) then configTree.environment else {};
                    config.networking = if (hasAttr "networking" configTree) then configTree.networking else {};
                    config.nix = if (hasAttr "nix" configTree) then configTree.nix else {};
                    config.nixpkgs = if (hasAttr "nixpkgs" configTree) then configTree.nixpkgs else {};
                    config.programs = if (hasAttr "programs" configTree) then configTree.programs else {};
                    config.security = if (hasAttr "security" configTree) then configTree.security else {};
                    config.services = if (hasAttr "services" configTree) then configTree.services else {};
                    config.systemd = if (hasAttr "systemd" configTree) then configTree.systemd else {};

This works fine, you can see it in action here, and eventually in the repo that gist links too. This is, of course, ugly, but it does work and avoids the infinite recursion. The mkOneLevelTree function in the linked gist does what I wanted, but ironically, it turns out I really needed a two-level tree, which is the same basic problem and could be adapted from the linked code, but I'm coming around to the notion that that doesn't really improve things in the way I hoped.

For future readers, I'm unlikely to return to this topic again, I suppose if you have specific questions about the series of breaks with reality that led to this delightful mess, just @ me and I'll try to respond. For now, I'm going to go out to the woods and think about what I've done. I'm very sorry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment