Adding more control over overriding default module definitions

I’ve been thinking about this a bit but thought I’d try to see if this is a relevant/wanted feature before I started hacking stuff together.

Currently if you want to modify defaults in the module system the only option (as far as I’m aware) is to use mkOverride. Given an option, the module system gathers all the definitions of this option, takes all the ones of lowest priority (which is given by mkOverride or is set to a high number for option defaults) and merges them. This works well if you want to just completely override the value of some option but the downside is that it completely discards anything of a higher priority.

This can be an inconvenience in some cases:

  • The first case is one that I’ve come across in my nix code which is where the default for an option is (for example) a list and I want to add an item to it. One way this can be done is by specifying my addition with mkOptionDefault but this feels like a hack to me and not really the intended behaviour. An example of this can be found at https://github.com/rycee/home-manager/blob/8bddc1adab0f7a51476f819fa2197353e8e1d136/modules/services/window-managers/i3.nix#L475 where the description recommends people use mkOptionDefault

  • A harder case to deal with is where you want to remove a package from a list of defaults. For example a couple of days ago someone asked in IRC how they would go about removing pkgs.nano from environment.systemPackages. Obviously overriding environment.systemPackages is a poor idea and I couldn’t see of another way of removing this without either forking nixpkgs or disabling the system path module and putting your own in. Whether or not removing nano from your system is a good idea, I thought the use case was interesting and did not think there was a canonical way to do this in nix.

I propose solving these sort of problems by the addition of a new function that could perhaps be called mkOverlay. I feel mkOverride is a better name but this is clearly taken. mkOverlay would take as input a function. What this would do is that when the module system finds a definition of this type, it would merge all inputs of higher priority and then it would pass these into the function that mkOverlay wraps. What this returns would then be merged with all items of equal priority (or overridden by items with a lower priority) as normal.

I’ll give an example to demonstrate this: Say as in the second bullet point above we want to remove nano from environment.systemPackages. Then in our configuration we would put:

with lib;
environment.systemPackages = mkForce (mkOverlay (super: 
  filter (x: x != pkgs.nano) super
));

Here mkForce puts this at a lower priority than all other definitions. Then the module system will gather all definitions with priority higher than 50 (the priority given by mkForce) and will process these as normal to get the environment.systemPackages one would have got without adding this. Then this is passed as super into the function body above and the function returns this with nano filtered out. As there are no other definitions with priority 50, the output of this function becomes the final merged value of environment.systemPackages.

Problems I see with my approach:

  1. Makes evalModules more complicated
  2. Over use of these could become quite complicated and hard to follow. I would recommend avoiding it’s use in nixpkgs.
  3. I don’t think the name is good
  4. It seems counterintuitive to me that you need to use both mkForce and mkOverlay. Perhaps mkOverlay, should consider definitions with it’s priority or higher instead of just it’s priority.

Interested to hear if anyone else has any thoughts on this, or if there is an alternative (and neater) solution to these sort of problems

2 Likes

Hi alexarice,
I like your idea for mkOverlay. It will be very powerful addition the module system. I would really like to see this implemented. My suggestion for the name is mkApply (named after the apply function from mkOption which does kind of the same thing). I attempted to implement this feature myself, and it almost works like you described. However it doesn’t yet consider values with a higher priority number (so only same priority). Feel free to use this in any way you like. You can find the diff for my implementation here: mkApply.patch · GitHub

1 Like

Perhaps the approach you have taken (of ignoring priority and just applying a function) is better. I think it is definitely simpler and I believe it fixes the issues I put above

Sorry for the bump, but what ended up happening here?

Nothing as far as I know

Do you want to create a pull request from the Gist?

Instead of using mkApply like this:

config.xxx = mkApply (x: doSomethingWith x);

why not set apply like this:

options.xxx = lib.mkOption { apply = x: doSomethingWith x; };

The only problem is that in the current implementation apply functions are not mergeable. I think we need to fix this.

I created a PR to make apply mergeable. It is similar to the mkApply proposal, except for that the PR reuses an existing mechanism.