lib.modules.mkIf vs lib.attrsets.optionalAttrs and other module system basics

I’m in the process of writing some Home Manager configurations after a long time away from touching NixOS modules, and realizing I don’t get some of the fundamentals of the module system. I wanted to override a default in the Home Manager module for Firefox only on Nix-Darwin, like I’ve done here:

{ pkgs, lib, ... }: with lib; {
  programs.firefox.enable = true;
  programs.firefox.nativeMessagingHosts = with pkgs; [
    tridactyl-native
  ];
} // mkIf pkgs.stdenv.isDarwin {
  programs.firefox.package = null;
}

But initially, I reached not for mkIf but for optionalAttrs, since that seemed to be the thing I wanted. However, that results in an evaluation error of infinite recursion. In playing around, I realized that these are okay:

{ pkgs, lib, ... }: with lib; {
  programs.firefox.enable = true;
  programs.firefox.nativeMessagingHosts = with pkgs; [
    tridactyl-native
  ];
} // optionalAttrs (true) {
  programs.firefox.package = null;
}
{ pkgs, lib, ... }: with lib; {
  programs.firefox = {
    enable = true;
    nativeMessagingHosts = with pkgs; [
      tridactyl-native
    ];   
  } // optionalAttrs pkgs.stdenv.isDarwin {
    package = null;
  };
}

but the following isn’t:

{ pkgs, lib, ... }: with lib; {
  programs.firefox.enable = true;
  programs.firefox.nativeMessagingHosts = with pkgs; [
    tridactyl-native
  ];
} // optionalAttrs (pkgs.stdenv.isDarwin) {
  programs.firefox.package = null;
}

I found this thread and its resolution relevant but I’m afraid I’m still missing something vital.

  1. Why does optionalAttrs cause problems for me when merging the outermost attribute set, the one being returned by the module, but not for more inner ones?

  2. Why am I getting infinite recursion when I refer to pkgs.stdenv.isDarwin (but not just (true)) in that outermost place with optionalAttrs, even though that module doesn’t apparently modify pkgs?

  3. Where should I get started in trying to really understand this stuff about the purpose and motivation of constructs like mkIf, and how the module system works more generally? I’ve poked around a bit in the Nixpkgs codebase and in the Nixpkgs and NixOS manuals but I think I could use some outside direction.

First point: if pkgs is defined as foo // bar, accessing pkgs.anything is going to require both foo and bar to be evaluated until they become attribute sets. Until that is the case, Nix can’t know if the expression foo // bar has an attribute anything or whether it refers to something from foo or something from bar.

Second point: evaluating optionalAttrs true { ... } will of course result in { ... }, but evaluating optionalAttrs false { ... } will result in the empty attribute set. Not to belabor an obvious point, but these are different values; the first might contain an attribute anything but the second definitely does not.

The combination of these two points means that, if pkgs is defined as foo // optionalAttrs pkgs.anything { ... }, you will get an infinite loop because in order to evaluate pkgs.anything, you must first evaluate the arguments to // deep enough to get attribute sets out; but in order to evaluate optionalAttrs pkgs.anything { ... } to get either { ... } or the empty attribute set, you must first evaluate pkgs.anything.

Third point: mkIf is different. The result of mkIf condition { attr = whatever; } is always a non-empty attribute set! It is equivalent* to { attr = mkIf condition whatever; }mkIf will distribute the condition over all of the attributes in its second argument, and it will do this without attempting to evaluate the condition first. So with the mkIf version of your configuration, no loop.

Similarly, if you’re using optionalAttrs in a // on an inner attribute, there also isn’t a loop as long as the condition of optionalAttrs doesn’t require that inner attribute with the // to be evaluated. pkgs can be evaluated to an attribute set without having to evaluate that inner // first, as long as any top-level // (or other functions or operators) can evaluate.

[*] Don't rely on this too closely; what mkIf actually returns is a node representing a deferred computation that is later handled by the module system, but for most practical purposes you can ignore this.
2 Likes

And in all of the usual modules systems, pkgs is so defined, and we can tell this without really delving into the source because nixpkgs.overlays in a module can change pkgs? I get the general idea here, but all this seems less straightforward to me than, e.g., infinite recursion I’ve encountered in defining overlays. Part of what I want to learn about is the machinery involved in the specific example case I ran into.

This is really helpful, thank you!

This, too!

Thank you so much; you’ve cleared up a lot for me very succinctly. :smiley:

I feel a bit better equipped now. I also would love any tips you have on where to start reading (in the code or in the docs) to get my bearings more broadly. I know that eval-modules is important, obviously, but that’s about it, hehe.

Ah, good catch, I was a little sloppy in this part of the explanation. It’s true that overlays can be involved but I think the actual story is that the value of pkgs is an output of the module system, specifically derived from the config value nixpkgs.pkgs (and nixpkgs.overlays if applicable). So in your example the truth is more like config is defined as the merge of a bunch of modules, and in particular your module (which is what a configuration file is) uses // at the top level, and the optionalAttrs in one of the arguments to // uses pkgs.whatever which is actually config.nixpkgs.pkgs.whatever (ignoring overlays), and so it’s the same problem but config is the root instead of pkgs. I think. :smile:

For this level of question, I don’t know of a better resource than knuckling down and studying nixpkgs/lib/modules.nix at 4d321931cfde971b3d791a95fb6d22c157ef233a · NixOS/nixpkgs · GitHub. It’s all there, if not in a terribly digestible form.

1 Like

Well, I guess I know what I need to do, then. Thanks again!

Oh, while I’m admitting sloppiness, I should have seen this earlier but I don’t actually think it’s kosher to use // with mkIf at all. It might work in this case but I think the safer thing is to use mkMerge to combine plain attrsets with the results of other mk* module functions. :sweat_smile:

1 Like

Great explanation…This should be in the Nix documentation! Or in the wiki ?

TLDR: lib.mkIf doesn’t assign anything if false and the options behind it are checked if they exist and lib.optionalAttrs always assigns an attrset and if true also the inner parts.