зеркало из
https://github.com/iharh/notes.git
synced 2025-11-01 22:26:09 +02:00
163 строки
6.4 KiB
Plaintext
163 строки
6.4 KiB
Plaintext
https://nixos.org/guides/nix-pills/override-design-pattern.html
|
|
|
|
The next design pattern is less necessary but useful in many cases and it's a good exercise to learn more about Nix.
|
|
|
|
# 14.1. About composability
|
|
|
|
Functional languages are known for being able to compose functions.
|
|
In particular, you gain a lot from functions that are able to manipulate the original value into a new value having the same structure.
|
|
So that in the end we're able to call multiple functions to have the desired modifications.
|
|
|
|
In Nix we mostly talk about functions that accept inputs in order to return derivations.
|
|
In our world we want nice utility functions that are able to manipulate those structures.
|
|
These utilities add some useful properties to the original value, and we must be able to apply more utilities on top of it.
|
|
|
|
For example let's say we have an initial derivation drv and we want it to be a drv with debugging information and also to apply some custom patches:
|
|
|
|
debugVersion (applyPatches [ ./patch1.patch ./patch2.patch ] drv)
|
|
|
|
The final result will be still the original derivation plus some changes.
|
|
That's both interesting and very different from other packaging approaches, which is a consequence of using a functional language to describe packages.
|
|
|
|
Designing such utilities is not trivial in a functional language that is not statically typed,
|
|
because understanding what can or cannot be composed is difficult.
|
|
But we try to do the best.
|
|
|
|
|
|
# 14.2. The override pattern
|
|
|
|
In the pill 12 we introduced the inputs design pattern.
|
|
We do not return a derivation picking dependencies directly from the repository,
|
|
rather we declare the inputs and let the callers pass the necessary arguments.
|
|
|
|
In our repository we have a set of attributes that import the expressions of the packages and pass these arguments, getting back a derivation.
|
|
Let's take for example the graphviz attribute:
|
|
|
|
graphviz = import ./graphviz.nix { inherit mkDerivation gd fontconfig libjpeg bzip2; };
|
|
|
|
If we wanted to produce a derivation of graphviz with a customized gd version,
|
|
we would have to repeat most of the above plus specifying an alternative gd:
|
|
|
|
mygraphviz = import ./graphviz.nix {
|
|
inherit mkDerivation fontconfig libjpeg bzip2;
|
|
gd = customgd;
|
|
};
|
|
|
|
That's hard to maintain. Using callPackage it would be easier:
|
|
|
|
mygraphviz = callPackage ./graphviz.nix { gd = customgd; };
|
|
|
|
But we may still be diverging from the original graphviz in the repository.
|
|
|
|
We would like to avoid specifying the nix expression again, instead reuse the original graphviz attribute in the repository and add our overrides like this:
|
|
|
|
mygraphviz = graphviz.override { gd = customgd; };
|
|
|
|
The difference is obvious, as well as the advantages of this approach.
|
|
|
|
Note: that .override is not a "method" in the OO sense as you may think.
|
|
Nix is a functional language.
|
|
That .override is simply an attribute of a set.
|
|
|
|
|
|
# 14.3. The override implementation
|
|
|
|
I remind you, the graphviz attribute in the repository is the derivation returned by the function imported from graphviz.nix.
|
|
We would like to add a further attribute named "override" to the returned set.
|
|
|
|
Let's start simple by first creating a function "makeOverridable" that takes a function and a set of original arguments to be passed to the function.
|
|
|
|
Contract: the wrapped function must return a set.
|
|
|
|
Let's write a lib.nix:
|
|
|
|
{
|
|
makeOverridable = f: origArgs:
|
|
let
|
|
origRes = f origArgs;
|
|
in
|
|
origRes // { override = newArgs: f (origArgs // newArgs); };
|
|
}
|
|
|
|
So makeOverridable takes a function and a set of original arguments.
|
|
It returns the original returned set, plus a new override attribute.
|
|
|
|
This override attribute is a function taking a set of new arguments,
|
|
and returns the result of the original function called with the original arguments unified with the new arguments.
|
|
What a mess.
|
|
|
|
Let's try it with nix repl:
|
|
|
|
$ nix repl
|
|
nix-repl> :l lib.nix
|
|
Added 1 variables.
|
|
nix-repl> f = { a, b }: { result = a+b; }
|
|
nix-repl> f { a = 3; b = 5; }
|
|
{ result = 8; }
|
|
nix-repl> res = makeOverridable f { a = 3; b = 5; }
|
|
nix-repl> res
|
|
{ override = "lambda"; result = 8; }
|
|
nix-repl> res.override { a = 10; }
|
|
{ result = 15; }
|
|
|
|
Note that the function f does not return the plain sum but a set, because of the contract.
|
|
You didn't forget already, did you? :-)
|
|
|
|
The variable res is the result of the function call without any override.
|
|
It's easy to see in the definition of makeOverridable.
|
|
In addition you can see the new override attribute being a function.
|
|
|
|
Calling that .override with a set will invoke the original function with the overrides, as expected.
|
|
|
|
But: we can't override again! Because the returned set with result 15 does not have an override attribute!
|
|
|
|
That's bad, it breaks further compositions.
|
|
|
|
The solution is simple, the .override function should make the result overridable again:
|
|
|
|
rec {
|
|
makeOverridable = f: origArgs:
|
|
let
|
|
origRes = f origArgs;
|
|
in
|
|
origRes // { override = newArgs: makeOverridable f (origArgs // newArgs); };
|
|
}
|
|
|
|
Please note the rec keyword.
|
|
It's necessary so that we can refer to makeOverridable from makeOverridable itself.
|
|
|
|
Now let's try overriding twice:
|
|
|
|
nix-repl> :l lib.nix
|
|
Added 1 variables.
|
|
nix-repl> f = { a, b }: { result = a+b; }
|
|
nix-repl> res = makeOverridable f { a = 3; b = 5; }
|
|
nix-repl> res2 = res.override { a = 10; }
|
|
nix-repl> res2
|
|
{ override = "lambda"; result = 15; }
|
|
nix-repl> res2.override { b = 20; }
|
|
{ override = "lambda"; result = 30; }
|
|
|
|
Success! The result is 30, as expected because a is overridden to 10 in the first override, and b to 20.
|
|
|
|
Now it would be nice if callPackage made our derivations overridable.
|
|
That was the goal of this pill after all.
|
|
This is an exercise for the reader.
|
|
|
|
|
|
# 14.4. Conclusion
|
|
|
|
The "override" pattern simplifies the way we customize packages starting from an existing set of packages.
|
|
This opens a world of possibilities about using a central repository like nixpkgs,
|
|
and defining overrides on our local machine without even modifying the original package.
|
|
|
|
Dream of a custom isolated nix-shell environment for testing graphviz with a custom gd:
|
|
|
|
debugVersion (graphviz.override { gd = customgd; })
|
|
|
|
Once a new version of the overridden package comes out in the repository, the customized package will make use of it automatically.
|
|
|
|
The key in Nix is to find powerful yet simple abstractions in order to let the user customize his environment with highest consistency and lowest maintenance time,
|
|
by using predefined composable components.
|
|
|