6
50
Fork 7
deep .override{Attrs} for nix, generalizes lib.pipe and recursiveUpdate
  • Nix 99.7%
  • Just 0.3%
Find a file
Adam Joseph d3f4e49112 BEHAVIOR CHANGE: restrict __input sugar to the named arguments of a function
Previously, you could infuse to `__input.foo` of a function which didn't have a
`foo` argument, so long as that function was a `{ ... }:`-function which doesn't
reject extraneous arguments.

Unfortunately this meant that there was no guard against spelling mistakes for
`__input` infusions.  Even worse, when infusing to a nixpkgs package, you might
infuse to an argument which it has today, but a later version of nixpkgs might
remove that argument -- in that situation, after the upgrade, the infusion would
silently do nothing.  This turned out to be the root cause of a very large
number of very frustrating bugs.

Therefore, this commit restricts the `__input` sugar -- it can only infuse to
explicitly-named arguments of a function.  If you really do want to infuse to a
non-existant argument of a `{ ... }:`-function, you'll have to do it manually by
calling `.override` yourself, without using the `__input` sugar.

Although this is a behavior change, it is of the kind that cannot silently cause
problems: what we're doing here is changing a behavior that previously did not
throw an exception so that it now throws an exception.  Anybody who relied on
the old behavior will find out immediately at eval time.  There are no "silent"
behavior changes from this commit.  Therefore, this is only a minor-version bump.
2026-06-14 16:04:19 -07:00
doc doc/commutative-diagram.md: replace applyAttrs with definition 2025-02-04 19:42:50 -08:00
examples init 2024-09-04 04:22:07 -07:00
tests BEHAVIOR CHANGE: restrict __input sugar to the named arguments of a function 2026-06-14 16:04:19 -07:00
.mailmap init 2024-09-04 04:22:07 -07:00
default.nix BEHAVIOR CHANGE: restrict __input sugar to the named arguments of a function 2026-06-14 16:04:19 -07:00
justfile init 2024-09-04 04:22:07 -07:00
README.md README.md: fix typo in example 2025-01-01 02:34:49 -08:00

infuse.nix

What?

infuse is a "deep" version of both .override and .overrideAttrs which generalizes both lib.pipe and recursiveUpdate. It can be used as a leaner, untyped alternative to lib.modules. If you want dynamic typechecking, it works well with yants. Infusion has specified semantics which preserve identity and associativity laws at all three of nix's non-finite types

Why?

Would you rather write this:

final: prev: {

  python311 = prev.python311.override
    (previousArgs: previousArgs // {
      packageOverrides =
        lib.composeExtensions
          (previousArgs.packageOverrides or {})
          (final: prev: {
            dnspython = prev.dnspython.overrideAttrs(previousAttrs: {
              doCheck = false;
            });
          });
    });

  xrdp = (prev.xrdp.override {
      systemd = null;
    })
    .overrideAttrs(previousAttrs: {
      env = previousAttrs.env or {} // {
        NIX_CFLAGS_COMPILE =
          (previousAttrs.env.NIX_CFLAGS_COMPILE or "")
          + " -w";
      };
      passthru = previousAttrs.passthru or {} // {
        xorgxrdp = previousAttrs.passthru.xorgxrdp
          .overrideAttrs (previousAttrs: {
            configureFlags = (previousAttrs.configureFlags or []) ++ [
              "--without-fuse"
            ];
          });
      };
    });
}

... or this?

final: prev: infuse prev {

  python311.__input.packageOverrides.__overlay.dnspython.__output.doCheck.__assign = false;

  xrdp.__input.systemd.__assign = null;
  xrdp.__output.env.NIX_CFLAGS_COMPILE.__append = " -w";
  xrdp.__output.passthru.xorgxrdp.__output.configureFlags.__append = ["--without-fuse"];

}

If you think the second expression is easier to read, write, and maintain, you would probably be interested in infuse.nix.

How?

The basic idea is that when its second argument is an attrset, infuse acts like lib.recursiveUpdate, except that in the second argument you must mark any subtrees where you want the automatic merging to stop. You mark those subtrees by changing them from whatever value they were into a function which returns that value (and ignores its argument).

In the following example, fred gets clobbered because we used //:

       { bob.fred = 3; } // { bob.jill =    4;        } == { bob.jill = 4; }

If we instead use infuse, we end up with both bob.fred and bob.jill, because we have marked bob.jill by making its value a function (_: 4):

infuse { bob.fred = 3; }    { bob.jill = _: 4;        } == { bob.fred = 3; bob.jill = 4; }

Unlike lib.modules, we can get back the "clobbering" behavior if we want, by marking an attribute higher up the tree:

infuse { bob.fred = 3; }    { bob = _: { jill = 4; }; } == { bob.jill = 4; }

This allows to merge attrsets containing structured values which should replace each other (or report an error) rather than getting mixed together.

Lists

When the second argument is a list, infuse acts like lib.pipe:

infuse
   { x = 3; }
   [ { x = x: x*x; } (fred: fred.x+1) ]
==
   10

You can even mix lists with attrsets:

infuse
   { bob.fred.x = 3; }
   { bob.fred = [ { x = x: x*x; } (fred: fred.x+1) ]; }
==
   { bob.fred = 10; }

There is No Magic Under the Hood

All those double-underscore attributes you see, like __input and __output are just sugar. You can omit them, or even define your own sugars:

let
  infuse = import ../default.nix {
    inherit lib;
    sugars = infuse.v1.default-sugars ++ lib.attrsToList {
      __concatStringsSep =
        path: infusion: target:
          lib.strings.concatStringsSep infusion target;
    };
  };
in
  infuse.v1.infuse
    { fred = [ "woo" "hoo" ]; }
    { fred.__concatStringsSep = "-"; }
  ==
    { fred = "woo-hoo"; }

The process of replacing these double-underscore attributes by expanded definitions is called desugaring. After desugaring, what's left is a desugared infusion: an attrset whose leaf values are all functions -- no integers, booleans, strings, etc. In other words: it is an error to try to infuse an attrset whose desugaring contains any non-function leaf values.

When you infuse a desugared infusion into a target, each function in the infusion is applied to the target attrvalue which has the same attrpath.

Semantics

Let's take a look at lib.pipe. It has two important properties:

  • flip pipe [] does the same thing as lib.id
  • flip pipe (a ++ b) does the same thing as flip pipe [ (flip pipe a) (flip pipe b) ]

Infuse has both of these properties as well, when used on lists:

  • flip infuse [] does the same thing as lib.id
  • flip infuse (a ++ b) does the same thing as flip pipe [ (flip infuse a) (flip infuse b) ]

What makes infuse special is that it also works on attrsets, and does so in the same way that it (and lib.pipe) work on lists:

  • flip infuse {} does the same thing as lib.id
  • flip infuse (a // b) does the same thing as flip pipe [ (flip infuse a) (flip infuse b) ]

In fact, infuse does the same trick for functions too!

  • flip infuse lib.id does the same thing as lib.id
  • flip infuse (pipe [ a b ]) does the same thing as flip pipe [ (flip infuse a) (flip infuse b) ]

Most important of all, these are not three separate tricks (one for lists, one for attrsets, and one for functions). It is one single trick that works at all three of the non-finite Nix types (lists, attrsets, and functions).

Examples

For an example of what infuse.nix can do, see amjoseph's overlays.