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

jfredett commented Jan 15, 2023

A NixOS Module Conundrum:

I have a pile of services which look like <some parent options> [<named sets of child options>] and I'm trying to encapsulate the whole thing as a function which produces a module, but I hit an infinite recursion ("error output") when trying to refer to the options in the config section. The parameterizedModule.nix contains the function definition which is intended to have a signature like: mkParameterizedModule :: Attrs -> Module, and the example-use.nix shows how it might be invoked to create a module that has definitions like:

{config, lib, pkgs, ...}: {
  #snip 
  pinky = {
    enable = true; 
    name = "test-service";
    subservices = {
      test-service2 = {
        startScript = "echo 'starting test-service2'";
        stopScript = "echo 'stopping test-service2'";
      };
      test-service = {
        startScript = "echo 'starting test-service'";
        reloadScript = "echo 'reloading test-service'";
        stopScript = "echo 'stopping test-service'";
      };
    };
  };
}

My question is, "Am I just doing this entirely wrong?" The intended usecase is for defining trees of systemd services that all relate back to each other, but it struck me as generic and the old Haskeller in me wanted to make some higher order goodness.

I suspect the answer is "don't do it this way", but I can't find another example of this kind of parameterized module?

@jfredett
Copy link
Author

jfredett commented Jan 17, 2023

The estimable Sandro over on mastodon helped me get what is going wrong here.

https://c3d2.social/@sandro/109702163301113735

"[@jfredett] I have two ideas:

  • Try adding --show-trace to see at which point it walks into itself
  • you are assigning in the 3rd file to config but need to evaluate config for that. Try only setting an attr of config.

If I understand the module system correct then it first collects all config attrs, evaluates them to the first level and then lazily walks them down until it can calculate the requested path/value."

The reason the above isn't working is very very simple.

# this:
config = mkIf parentConfigOptions.enable (confs config.${familyName} config.${familyName}.${childrenName});
# is just this;
config = blahblahFunctions config.something
# with extra steps

This is very very obviously not going to work, because in order to know what config.something is, you have to look up config, which is defined in terms of config.something. Something about forest and trees.

In any case, it seems there is a restriction that means you can't assign full control over config while referring to your options to a returned function. You can either defer all your config manipulation to the end, or you can do it all right now, a HOF can only do option magic, really.

This makes sense, since the NixOS build process is something like: do imports, configure options, construct configuration. This HOF is trying to exist in a stage that comes right before the last, configuring the configuration -- at least inasmuch as doing something has to refer to options.

There might be some tricks for this to force an early evaluation of a specific part of the config. Something must collect the values that doesn't have infinite recursion problems, so maybe there is a way to get at that. I do not know the answer to these things, but I'll post again if I find them (and remember).

@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