2025-02-06 published | 2025-04-17 edited | 3173 words
These are some notes about the Nix language:
Always consider { } like a Python dictionary. It’s
not a block of code, even when it spans over many many lines. It’s an
attribute set.
Also in the function definition or a function call.
{ } looks like arguments and works like arguments, but it’s
due to pattern matching. { } is a single attribute
set.
What is before . is an attribute set. What follows
is the name of the attribute inside that attribute set. The
foo.bar represents value of the attribute bar
inside the attribute set foo.
Look out for : it is a function definition.
{ } : { }Did you know that you can assign a name to a function? I mean…
funName = { } : { } ;Nix is declarative, so = is more like “what is on
left is the same as what is on right.”
If you wanna more parameters in the function, you can curry – define function returning function
{ } : { } : { }and of course name it
funName = { } : { } : { } ;Don’t forget { } is an attribute set. Let’s define a
function accepting two attribute set arguments (i.e., function accepting
a single attribute set returning another fuction accepting a single
attribute set, in fact,) that always return 1. Let’s call
that function with two empty attribute sets as its arguments:
(_: _: 1) { } { }_: _: 1 is the function definition.
( ) only say about precedence when it’s not
obvious.
{ } { } are function’s attribute set
arguments.
You can use _ as function parameter if you don’t
care about the parameter.
Beware of ; (TL;DR is that ; finishes
=, with, and inherit.)
; is used when giving names to functions
funName = { } : { } ;but not in nix repl
funName = { } : { }and not when calling functions
funName { }It is used inside attribute set (after the last one, too)
{
bar = foo.bar ;
buzz = foo.buzz ;
}but not inside attribute set when defining a function accepting
attribute set (where , is used but not after the last
one)
{ bar, buzz } : bar + buzzbut yes inside the attribute set argument when calling that function (but not at the end of function call as already mentioned)
({ bar, buzz } : bar + buzz) { bar = "foo"; buzz = "foo"; }{ bar, buzz } : bar + buzz is the function
definition.
{ bar, buzz } is function’s attribute set parameter.
It’s called argument set.
bar + buzz is function’s implementation.
( ) only say about precedence when it’s not
obvious.
{ bar = "foo"; buzz = "foo"; } is function’s
attribute set argument.
Function definition has single attribute set parameter
{ bar, buzz }. Function is called with the single attribute
set argument { bar = "foo"; buzz = "foo"; }. The parameter
is pattern matched to the argument.
It looks like the function has two parameters bar
and buzz but it’s single attribute set, in fact. Neither
it’s currying.
You can shortcut attribute set
{
bar = foo.bar ;
buzz = foo.buzz ;
}to attribute set
{
inherit (foo) bar buzz ;
}Nix language is ugly; it’s hard to read. (Only my opinion, of
course.) I will take (probably non-working) flake.nix,
shape it, and explain the sections as I understand them.
Also, reading the code from top to bottom is a bit misleading, because Nix is declarative. It means that by writing Nix code, the relations are specified, not the steps or commands.
{
outputs = {
self,
nixpkgs,
flake-utils,
...
}: let
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
name = "package_name";
version = "0.1.0";
pypkg = {
buildPythonPackage,
pkgs,
}:
buildPythonPackage {
pname = name;
inherit version;
nativeCheckInputs = [pkgs.ruff];
};
in
{
overlays.default = final: _: {
"${name}" = final.callPackage pypkg {};
};
}
// eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default;
in {
packages.default = pkgs.python3Packages."${name}";
legacyPackages = pkgs;
devShells = filterPackages system {
default = pkgs.mkShell {
packages = with pkgs; [
ruff
];
};
};
});
}Let’s reshape the code, because { } is attribute set and
: defines functions and that’s important. There are also
let ... in ... and with ... blocks that
benefit from different indentation.
{
outputs =
{
self,
nixpkgs,
flake-utils,
...
}
:
let
inherit (flake-utils.lib) eachDefaultSystem filterPackages ;
name = "package_name" ;
version = "0.1.0" ;
pypkg =
{
buildPythonPackage,
pkgs,
}
:
buildPythonPackage
{
pname = name ;
inherit version ;
nativeCheckInputs = [ pkgs.ruff ] ;
}
;
in
{
overlays.default =
final
:
_
:
{
"${name}" = final.callPackage pypkg {} ;
}
;
}
//
eachDefaultSystem
(
system
:
let
pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default ;
in
{
packages.default = pkgs.python3Packages."${name}" ;
legacyPackages = pkgs ;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages =
with pkgs ;
[
ruff
]
;
}
;
}
;
}
)
;
}Indent 0
{
...
}flake.nix is an attribute set.Indent 2
outputs =
...
:
...
;The flake has (in our case single) outputs
attribute.
outputs is a function.
There are two blocks at indent 4!
Indent 4, 1st block
{
self,
nixpkgs,
flake-utils,
...
}Input parameter to the outputs function is an
attribute set.
The outputs function expects to be called with the
attribute set containing at least self,
nixpkgs, and flake-utils.
However, Nix is declarative. It’s probably better to say that
outputs function will define relations to at least
self, nixpkgs, and
flake-utils.
self is a special parameter denoting the whole flake
itself. That may sounds bizzare, but Nix is declarative and it’s
possible to define relationsip between itself and its parts.
self is recursive.
We can say that we define self.outputs, a function that
takes self as one of the inputs.
nixpkgs are NixOS packages available on the
internet.
flake-utils are (nix flake related) functions
published on the internet.
... means there could be more arguments provided to
the outputs function when the function is called.
Indent 4, 2nd block
let
...
in
...The result of the let ... in ... construct is what
is after in but in that in ... it’s possible
to use what was defined in the let ....
It’s like if the relations defined in let ... are
considered as the local definitions at the top of
in ....
The reason for let ... in ... is that things in
let ... can be mutually recursive – one thing can depend on
the other one that depends on the first one.
There are two blocks at indent 6!
Indent 6, 1st block (let ...)
inherit (flake-utils.lib) eachDefaultSystem filterPackages ;
name = "package_name" ;
version = "0.1.0" ;
pypkg =
...
:
...
;We are in the let ..., so the relations defined here
will be available in the in ....
inherit (flake-utils.lib) eachDefaultSystem filterPackages ;The shortcut for
eachDefaultSystem = flake-utils.lib.eachDefaultSystem ;
filterPackages = flake-utils.lib.filterPackages ;In the in ..., we can use
eachDefaultSystem and filterPackages instead
of the full names flake-utils.lib.eachDefaultSystem and
flake-utils.lib.filterPackages, respectively.
name = "package_name" ;
version = "0.1.0" ;In the in ..., we can use name instead
of "package_name" and version instead of
"0.1.0".
It’s like variables.
pypkg =
...
:
...
;In the in ..., we can use pypkg, which
is the function.
There are two blocks at indent 8!
Indent 8, 1st block (let ...,
pypkg = ...)
The input parameter to the pypkg function is attribute
set:
{
buildPythonPackage,
pkgs,
}pypkg function is going to do something (define
relations) with buildPythonPackage and
pkgs.Indent 8, 2nd block (let ...,
: ... ;)
The body of the pypkg function (pypkg
function definition, which is between : and ;)
consist of function call:
buildPythonPackage
{
pname = name ;
inherit version ;
nativeCheckInputs = [ pkgs.ruff ] ;
}The function called is buildPythonPackage, so what
pypkg returns is whatever buildPythonPackage
returns.
The buildPythonPackage is called with the single
argument, which is attribute set.
The attribute set argument consist of:
pname = name ;so inside the buildPythonPackage, pname
will be the same as name.
inherit version ;which is a shortcut to
version = version ;and
nativeCheckInputs = [ pkgs.ruff ] ;which says that whenever nativeCheckInputs is used
inside the buildPythonPackage, it is a list containing a
single element pkgs.ruff.
pkgs.ruff is whatever is the value of the
ruff attribute in the pkgs attribute
set.
Indent 6, 2nd block (in ...)
This is the second part of indent 6 that started with the
let ... block.
What is in in ... is in fact the output of the
outputs function in the flake’s attribute set. So this is
the main part.
There are two blocks in in ...:
...
//
...The // operator works on two attribute sets, therefore
both blocks at indent 8 are attribute sets.
The // operator is update. It updates the content of the
first attribute set with the content of the second one.
So the result is all the names and values from the first attribute set and from the second attribute set, and when a name is in both, the name and the value form the second attribute set is used.
What is in the in ... block is attribute set.
Therefore, the output of the outputs function is an
attribute set.
There are two blocks at indent 8!
Indent 8, 1st block (in ...,
... //)
The attribute set in the first block at indent 8 is the first part of
the output of the outputs function in the flake’s attribute
set. This attribute set will be updated with the second one, the one
which follows the // operator.
{
overlays.default =
final
:
_
:
{
"${name}" = final.callPackage pypkg {} ;
}
;
}The attribute set contains another attribute set
overlays.
The overlays attribute set contains
default attribute, which is a function.
The overlays.default function is defined with two
input parameters, final and _.
The result of the call to the overlays.default
function is the attribute set
{
"${name}" = final.callPackage pypkg {} ;
}There is a lot in
{ "${name}" = final.callPackage pypkg {} ; } attribute
set.
"${name}" means that the attribute set will contain
an attribute with the name given by whatever is name. In
this case name = "package_name" ; and therefore the
attribute set will contain the attribute
package_name.
"${name}" = ... ; says what will be the value of the
package_name attribute.
final.callPackage pypkg {} is the function call with
the first argument being function pypkg and the second
argument being empty attribute set { }.
So the value of the package_name attribute is what
is returned by the final.callPackage function.
First, final and _. These relate to
overlays. Overlays are things that are stacked on top of each other,
augmenting the previous one. The final is the overlay after
applying all overlays. At the place of _, there is usually
prev, but if it’s not needed, the _ is
used.
How can I know which overlay is final when I even don’t
know now what is previous? Because Nix is declarative. It is possible to
specify the relation to the final whenever. Therefore,
final.callPackage means take callPackage from
whatever is the last overlay.
callPackage takes two arguments. First argument is a
function that builds a package, pypkg in our case. Recall
that pypkg returns whatever buildPythonPackage
returns.
The second argument to the callPackage is an attribute
set of what should be overriden when calling pypkg.
{ } says that nothing is overriden.
From the above, the package_name is the package built
using the buildPythonPackage function.
To recap actual knowledge, the flake is an attribute set containing
outputs, which is a function returning an attribute set
containing overlays, which is an attribute set with
package_name with the value of the result of the call to
the buildPythonPackage function.
Indent 8, 2nd block (in ...,
// ...)
The attribute set in the second block at indent 8 updates the first one. Usual reason for this is that in the first block, there are system-independent things and in the second, there are system-dependent things.
eachDefaultSystem
(
...
:
...
)Function eachDefaultSystem is called with the single
argument. That argument is a function.
( ) only says about precedence, because it’s not
obvious in this case.
The function in the argument of eachDefaultSystem
takes single input parameter – string denoting the system, e.g.,
"x86_64-linux".
The function in the argument of eachDefaultSystem
needs to return an attribute set.
eachDefaultSystem takes the attribute set returned
by the function it has as the argument, and changes every name of the
attribute in such way that the foo = ... will become
foo."${system}" = ..., e.g.,
foo.x86_64-linux = ....
This will happen for each default system:
"aarch64-linux", "aarch64-darwin",
"x86_64-darwin", and "x86_64-linux".
In other words, if the function in the argument of
eachDefaultSystem returns the attribute set
{
foo = "bar" ;
}the result of
eachDefaultSystem
(
_
:
{
foo = "bar" ;
}
)is the attribute set
{
foo.aarch64-linux = "bar" ;
foo.aarch64-darwin = "bar" ;
foo.x86_64-darwin = "bar" ;
foo.x86_64-linux = "bar" ;
}There are two blocks at indent 10!
Indent 10, 1st block (in ..., // ...,
( ...)
This is system, the input parameter to the function,
which is the first argument passed to the eachDefaultSystem
function.
Indent 10, 2nd block (in ..., // ...,
: ... ))
There is let ... in ... in the second block at indent
10. Take first let ....
pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default ;So in in ..., it’s possible to use pkgs
instead of
nixpkgs.legacyPackages.${system}.extend self.overlays.default
whenever appropriate. Now, what is
nixpkgs.legacyPackages.${system}.extend self.overlays.defaultThis is a function call.
Function extend from the
nixpkgs.legacyPackages.${system} is called with the single
argument self.overlays.default.
Function extend stacks overlays.
The result is the final overlay to be stored in
pkgs.
The result, the final overlay, the pkgs, is the
attribute set. This attribute set contains all the packages from the
nixpkgs for the "${system}".
The result pkgs also contains
package_name package, because we extend it
with self.overlays.default.
self refers to the flake itself.
self.overlays refers to the overlays in the
flake’s outputs function.
self.overlays.default is the function returning
attribute set with package_name.
So in the in ..., we can use pkgs, which is
the attribute set containing all nixpkgs for the
"${system}" and also package_name.
Finally, in the in ..., there is
{
packages.default = pkgs.python3Packages."${name}" ;
legacyPackages = pkgs ;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages =
with pkgs ;
[
ruff
]
;
}
;
}
;
}A reminder that
packages.default = pkgs.python3Packages."${name}" ;
legacyPackages = pkgs ;will became
packages."${system}".default = pkgs.python3Packages."${name}" ;
legacyPackages."${system}" = pkgs ;for each system, i.e.,
packages.aarch64-linux.default = pkgs.python3Packages."${name}" ;
legacyPackages.aarch64-linux = pkgs ;
packages.aarch64-darwin.default = pkgs.python3Packages."${name}" ;
legacyPackages.aarch64-darwin = pkgs ;
packages.x86_64-darwin.default = pkgs.python3Packages."${name}" ;
legacyPackages.x86_64-darwin = pkgs ;
packages.x86_64-linux.default = pkgs.python3Packages."${name}" ;
legacyPackages.x86_64-linux = pkgs ;and will be a part of the overall flake’s output, i.e., the part of the attribute set described in “Indent 6, 2nd block”.
packages.default = pkgs.python3Packages."${name}" ;Make the package_name build by the
buildPythonPackage the default package. It roughly means
that nix run will execute
package_name.
packages is along with overlays one of
the well-known flake outputs.
legacyPackages = pkgs ;Make the legacyPackages, which is a list of packages
provided by this flake, the same as the pkgs.
legacyPackages is roughly the same as
packages, but faster to work with.
legacyPackages is along with packages
and overlays one of the well-known flake outputs.
The last is also well-known flake output, devShells,
which is activated by the nix develop command.
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages =
with pkgs ;
[
ruff
]
;
}
;
}
;Assign devShells the result of the call to the
filterPackages function.
The result of the call to the filterPackages
function is the attribute set representing packages that are available
for the system.
filterPackages function is called with two
arguments. First is the system, e.g., “x86_64-linux”. (Do
not forget that we are in eachDefaultSystem. Consequently,
filterPackages will be called for each default system –
four times in this case.)
The second argument of the call to the filterPackages
function is an attribute set representing packages being
filtered.
The default of the packages of the attribute set
argument is what will be run for nix run, respectively
nix develop command in this case.
The default is the result of the call to
pkgs.mkShell. Therefore, nix develop command
runs the result of the pkgs.mkShell.
pkgs.mkShell takes as the argument the attribute set
with the packages attribute.
The value of the packages attribute is the list of
the packages meant to be available in the development shell started by
the nix develop command.
with pkgs ;
[
ruff
]is the shortcut for
[
pkgs.ruff
]usually used when there is more packages from pkgs in
the list.
devShells is along with legacyPackages,
packages, and overlays one of the well-known
flake outputs.
I don’t think this is awesome. I think this is really bad.
go back | CC BY-NC-SA 4.0 Jiri Vlasak