2025-02-05
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 ;
it 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 + buzz
but 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 =
.mkShell
pkgs{
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 ...
)
(flake-utils.lib) eachDefaultSystem filterPackages ;
inherit
"package_name" ;
name = "0.1.0" ;
version =
pypkg =...
:
... ;
We are in the let ...
, so the relations defined here
will be available in the in ...
.
(flake-utils.lib) eachDefaultSystem filterPackages ; inherit
The shortcut for
-utils.lib.eachDefaultSystem ;
eachDefaultSystem = flake-utils.lib.filterPackages ; filterPackages = flake
In the in ...
, we can use
eachDefaultSystem
and filterPackages
instead
of the full names flake-utils.lib.eachDefaultSystem
and
flake-utils.lib.filterPackages
, respectively.
"package_name" ;
name = "0.1.0" ; version =
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
[ pkgs.ruff ] ; nativeCheckInputs =
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 ...
.
.legacyPackages.${system}.extend self.overlays.default ; pkgs = nixpkgs
So in in ...
, it’s possible to use pkgs
instead of
nixpkgs.legacyPackages.${system}.extend self.overlays.default
whenever appropriate. Now, what is
.legacyPackages.${system}.extend self.overlays.default nixpkgs
This 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 =
.mkShell
pkgs{
packages =
with pkgs ;
[
ruff]
;
}
;
}
;
}
A reminder that
.default = pkgs.python3Packages."${name}" ;
packages legacyPackages = pkgs ;
will became
."${system}".default = pkgs.python3Packages."${name}" ;
packages."${system}" = pkgs ; legacyPackages
for each system, i.e.,
.aarch64-linux.default = pkgs.python3Packages."${name}" ;
packages.aarch64-linux = pkgs ;
legacyPackages.aarch64-darwin.default = pkgs.python3Packages."${name}" ;
packages.aarch64-darwin = pkgs ;
legacyPackages.x86_64-darwin.default = pkgs.python3Packages."${name}" ;
packages.x86_64-darwin = pkgs ;
legacyPackages.x86_64-linux.default = pkgs.python3Packages."${name}" ;
packages.x86_64-linux = pkgs ; legacyPackages
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”.
.default = pkgs.python3Packages."${name}" ; packages
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 =
.mkShell
pkgs{
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
[
.ruff
pkgs]
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