Yet another infinite recursion problem (configurable imports)

Man, I’m really getting tired of these infinite recursion problems. Each time I think I’ve got some new way to sort them out with mkIf / lib.optionals / mkMerge / options ? missing_thing / _module.args / specialArgs… I just keep getting stuck on them.

{
  inputs.nixpkgs.url = "github:nixos/nixpkgs";
  outputs = {
    self,
    nixpkgs,
  } @ inputs: {
    nixosConfigurations.fake = nixpkgs.lib.nixosSystem {
      specialArgs = {
        inherit inputs;
        isLinux = true;
      };
      modules = [
        ./hardware-configuration.nix
        ({
          lib,
          isLinux ? false,
          isDarwin ? false,
          ...
        }: {
          imports = [] ++ lib.optionals isLinux [] ++ lib.optionals isDarwin [];
        })
      ];
    };
  };
}

Why does this give me infinite recursion on isDarwin? Shouldn’t isDarwin ? false take care of that?

BONUS: Is there a better way for me to show a MRE for a module problem? Perhaps a very simple VM config?

1 Like

Also, between this forum, nixpkgs issues, and stackoverflow, this seems like a really common (and IMO really frustrating) problem. Would a page reviewing the issue (particularly with config, which seems to be a common one) be a reasonable addition to the nix.dev FAQ? Could include links to some of the best answers and workarounds, and touch on the strategies I mentioned above (_module.args, specialArgs, mkMerge / mkIf, etc.)

1 Like

You can’t use default values in modules, the module system will always try to set a value through _module.args which it can’t and tries to find a solution.

In general, this doesn’t make sense at all, a nixosConfiguration is always linux, and never darwin.

As another tip: you probably do not want optional imports, instead you want to selectively fill the modules list or unconditionally pass all modules, and enable them selectively.

2 Likes

You can’t use default values in modules, the module system will always try to set a value through _module.args which it can’t and tries to find a solution.

Huh. This is surprising – do you know where I can learn more about why this is the case? Are there other places that default args wont work?

In general, this doesn’t make sense at all, a nixosConfiguration is always linux, and never darwin.

Of course, this was a MRE for a much larger configuration for a mix of Darwin and Linux x86_64 and aarch64 machines. Some of the modules work on all configs, some are platform specific.

As another tip: you probably do not want optional imports ,

Why wouldn’t I want that, if I could get it to work?

instead you want to selectively fill the modules list or unconditionally pass all modules, and enable them selectively.

The former makes for a lot of manual repetitive editing, which is what I’m currently doing and for which I’m trying to find a more ergonomic approach. The latter is what I’m trying to accomplish, but the .enable approach is insufficiently lazy due to absence of several platform-specific options (launchd vs systemd for example), and mkIf chokes on the invalid attribute names when evaluated on the wrong platform (even if not enabled unfortunately).

I would be quite pleased if the laziness allowed .enable to solve this problem! But alas.

1 Like

Basically anywhere, where you build a fixed point where any argument might be set by the result of the fixed point.

I do not get “launchd” vs. “systemd”. NixOS will never have launchd, on nix-darwin you will never have systemd.

And trying to write modules that work for both is deemed to fail. It works for simply things, but only with a lot of care. You probably do not want to do that.

Because they are a massive foot gun. It is very easy to create infinite recursion with them, even if you do have a very good understanding of the module system.

1 Like

Ah ok – which is why you brought up _module.args – because it can’t know if the fallback value was overridden until the body is evaluated (because _module.args could specify a value), and the body cant be evaluated without knowing the value of the args. Something like that?

(I’ve read the Wikipedia entry for fixed point like 10 times and still don’t grok it – Will read again.)

Exactly like you’ve said. Even behind a mkIf cfg.enable, referring to launchd on Linux fails to evaluate, which is why I wanted to conditionally import that module in the first place. Other modules do not rely on platform-specific options and should imported regardless.

1 Like
modules = [
  ./configuration.nix
]
++ modulesWorkingForBoth
++ modulesOnlyWorkingForLinux;

As said, just selectively put them in the modules list, not sure how that creates “repretitive editing”… For me this looks like even less editing than having the list copied everywhere…

1 Like

Yes, that’s what I figured I’d have to do. The “repetitive editing” was referring to my current approach, which is importing each module separately (imports = [ ./service1.nix ./service2.nix ./service3.nix ];), which means that I’m going through and editing to importing that file on each configuration where I want it available, then editing to enable that module later in the config. Contrast that with a single top-level imports = [./services] across all machines, and then I would just have to enable the service on the machine (and it should already be there) – that’s what I was going for.

The other downside (relative to an isLinux argument) is that it’s just really handy to have in multiple places in my config (e.g. homeDirectory = if isLinux then "/home/n8henrie" else "/Users/n8henrie";) – it would be nice to have a reliable way to configure both imports and additional logic based on platform without having to worry about getting bitten by infinite recursion all the time.

Thanks again for your time and help.

1 Like

You can very easily write an option that acts like that:

{lib, ...}: {
  options.local.os = lib.mkOption {
    type = lib.types.str;
  };
}

Then set that to "linux" or "macos" in your OS-specific parent module, and then use homeDirectory = if (config.local.os == "linux") then "/home/n8henrie" else "/Users/n8henrie". Hell you can call it isLinux and make it a bool if you prefer.

This looks like a lot of effort for just that use case, but you’ll likely have more Linux- and MacOS-specific settings, and if you chuck all those in a module like that suddenly it’s all quite manageable and sensible. You might even find other similar booleans that describe “classes” of devices, and now you can make one for each of those things…

Well, either way, lots of solutions besides conditional imports, one of them should suit your tastes.

2 Likes

lib.importApply: init by roberth · Pull Request #230588 · NixOS/nixpkgs · GitHub adds importApply

        # lib.nix
        imports = [
          (lib.importApply ./module.nix { bar = bar; })
        ];
        # module.nix
        { bar }:
        { lib, config, ... }:
        {
          options = ...;
          config = ... bar ...;
        }
1 Like

I just realized that you are using flakes, so instead of adding paths to the modules list, you can add all your modules to the nixosModules/darwinModules outputs respectively, and do modules = [ ./hostRelatedMain.nix ] ++ builtins.attraValues self.nixosModules;.

1 Like

That makes perfect sense, so far I had only configured options in the context of a specific importable module (i.e. creating options.services.foo for a systemd service), I had never thought that this could be used as some kind of custom global variable. I assume that the options.local prefix is conventional / arbitrary? In other words no special reason to use options.local.foo as opposed to options.mine.foo?

Thanks for the suggestion!

Oh interesting, I hadn’t thought of that! I ended up just making separate services/{linux,darwin}, all 3 (including the parent) each with their own default.nix; the parent directory imports universal modules. It seems to work well. Should have reached for this sooner, thanks for the help.

Yep, it’s just the name I use, and I think it comes from me thinking “localhost”, which makes zero sense. Do please come up with a better name :wink:

I tend to use lib.nobbz.$name for “untyped” data that I just want to somehow get through the module system in an ad-hoc manor. Or when needing some “quick” option to try something out.

Usually I refactor them out again after a couple of days into proper options, which I simply move into the common programs or services tree.

In fact I try to reuse the top-level hirarchy as much as possible.

Yes, it is annoying when it eventually clashes, this happened to me about 1 time so far.