2025-03-07 published | 2025-04-30 edited | 11629 words
If you have to, you have to. (Deal with Nix and flakes.)
This is a summary of how we use Nix flakes for creating environments with all dependencies necessary for development, for distributing packages and applications, and for NixOS deployments.
We can’t avoid short summary of Nix language and an overview of Nix ecosystem. These are more like prerequisities, but help with the understanding of what to expect (from Nix ecosystem) and how to read (Nix language).
We expect the reader to be open-minded – be ready for things that do not make sense when read for the first time, like “the result of the function evaluation is the input to that function.”
When we talk about Nix, we should now what we are talking about. Nix can be a lot of things but let’s stop calling everything “Nix” for now and distinguish between Nix store, Nix language, Nix build tool, Nixpkgs, and NixOS.
At the bottom, Nix store contains so called derivations – recipes with “how to” create products – and products. It does not matter what product is; it is probably a Python package, executable application, NixOS configuration, or just some directory with text files.
We do not manipulate Nix store directly. There are plumbing commands
for that, but we don’t use them neither. We focus on source code in
.nix files written in Nix language and
related porcelain commands.
Nix language source code can also be written in a
flake.nix file. Flakes can be a lot of things, but let’s be
strict: A Nix flake is a directory that contains
flake.nix. And there are restrictions on the anatomy
of flake.nix file.
Nix build tool, the porcelain commands, are used to
manipulate Nix store to satisfy what is written in Nix language. In
other words, these commands instantiate source code in
.nix files by creating .drv derivations and
then realize products.
*.nix -------------> /nix/store/*.drv -----------> /nix/store/*
instantiation realization
(porcelain) (plumbing)
(The bottom is on right.)
The biggest known collection of .nix files is called
Nixpkgs – a single Git repository that tends to contain
all the packages. A package is what will eventually became a
product. The defining feature of Nixpkgs is overlays and here is the
manual.
Finally, NixOS is a Linux distribution based on Nixpkgs. It provides operating system environments and services. The defining feature of NixOS is modules and here is the manual.
To complement the related work, there is a good starting place for new Nix users, although outdated (2022) and unfinished, and the famous classic introduction to Nix from 2015.
Nix language is functional, pure, and lazy, but most importantly – declarative. It means that:
Functions are nothing special and can be considered the same as numbers or strings.
There is no way how to perform side effect, i.e., how to change state of anything. There is no assignment operator.
Nothing is evaluated until necessary.
It is not possible to write commands in Nix language, only how things relate to each other.
To perform side effects, there are Nix build tool commands that –
based on the source code in .nix files – manipulate Nix
store.
Beware of Nix language syntax:
[ ] is a list where elements are separated by
space.
( ) is only used to specify precedence.
= is just for naming things.
; is used solely to finish = or
with or inherit.
{ } is an attribute set – the basic data
structure.
It may span multiple lines and it contains attributes and their values:
{
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
}Before . is an attribute set, after . is
the name of the attribute, and the result of evaluation is the attribute
value, so
{ SQLAlchemy = "sqlalchemy"; }.SQLAlchemy == "sqlalchemy"is true. Moreover, strings are sometimes automatically
converted, so
{ "SQLAlchemy" = "sqlalchemy"; }."SQLAlchemy" == "sqlalchemy"is the same and also true.
There are shortcuts for attribute sets. This one:
{
SQLAlchemy = other.SQLAlchemy;
types-PyYAML = other.types-PyYAML;
}is the same as
{
inherit (other) SQLAlchemy types-PyYAML;
}which is the same as
with other; {
SQLAlchemy = SQLAlchemy;
types-PyYAML = types-PyYAML;
}And there is // to merge two attribute sets, where the
attributes from the attribute set on the right side are used in case of
conflict. ( ) are needed to specify the precedence:
({
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
} // {
SQLAlchemy = false;
}).SQLAlchemy == falseis true.
: is a function definition.
Functions have exactly one parameter. By convention, use
_ as parameter name to ignore the given argument.
The parameter name is on the left side of : and the
function implementation is on the right side.
The function is applied to an argument or called with an argument.
Note that there is a difference between function application and function evaluation – the function is applied, then evaluated. Applied function can be evaluated lazily.
Sometimes the precedence needs to be stated explicitely. The following is a function that always returns 1, applied to an empty attribute set and compared to 1.
(_: 1) {} == 1Because functions are nothing special, this is a list with two elements – function always returning 1 and empty attribute set:
[ (_: 1) {} ]To obtain a list with a single element, the result of the function application, we again need to state precedence explicitely:
[ ((_: 1) {}) ]It is possible to name the function.
justOne = _: 1;If a function needs to accept more than a single argument, we can use currying:
sumBoth = first: second: first + second;
sumBoth 3 4 == 7Because of currying, we also can use partial application:
addThree = sumBoth 3;
addThree 4 == 7Very often a function is applied to and/or returns an attribute set. Attributes in the input attribute set are pattern-matched and used as variables in the function implementation. This makes an illusion of input parameters and therefore it’s confusing.
Also very often, immediately after the : there is
let ... in .... Then the result of the function is what is
after in but it may use names from let ...
along with attributes of the input attribute set.
Although let ... in ... is a good practice, there are
now at least three sections in the code (hopefully properly indented)
for a single function – attribute names of the input attribute set,
names bound in let ..., and the function implementation in
in .... At least because the result of the
function used to be an attribute set and we can //, which
we use to support multiple platforms. (First attribute set contains
platform-independent attributes, then // another attribute
set containing platform-dependent attributes.)
The following is a function accepting a single attribute set argument
containing attributes with names self and
nixpkgs. Because of ... it may also contain
additional attributes.
In the let ... the meaning is given to the
attrValues, getAttrs, and
pypi-name-to-nix-name names.
The function returns an attribute set that is just after the
in. The attribute set has a single attribute
packages, which is again an attribute set – the result of
the application of getAttrs function to the list of strings
(attrValues pypi-name-to-nix-name) and the attribute set
(all Python3 packages from nixpkgs).
{
self,
nixpkgs,
...
}: let
inherit (nixpkgs.lib) attrValues getAttrs;
pypi-name-to-nix-name = {
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
in {
packages =
getAttrs
(attrValues pypi-name-to-nix-name)
nixpkgs.python3Packages;
}getAttrs
is a function that takes a list with names as the first argument and an
attribute set with package names and their derivations as the second
argument. It returns the attribute set with attributes (packages) from
the second argument whose names are listed in the first argument.
atttrValues
is a function that takes an attribute set as the only argument and
returns the list made up by the values of all attributes.
The biggest confusion when reading the Nix language, in my opinion, is made by mis-indentation. It is too easy to make nested function call over multiple lines with no first-sight-clear demarkation where arguments start and end. Take for example slightly rewritten part of the above example:
in {
packages = getAttrs (attrValues
pypi-name-to-nix-name)
nixpkgs.python3Packages;
}Even the example passes the nix fmt . sanity check, it
makes me insane.
For details about the Nix language, consult the Nix language tutorial and Nix language manual.
Let’s make a Nix flake for development, i.e. a directory containing
flake.nix file where we can call nix develop
and get all the dependencies we need. This is about dependencies for a
Python3 project.
$ nix develop
path '/my/repo' does not contain a 'flake.nix', searching up
error: could not find a flake.nix file$ cat flake.nix ; nix develop
error: syntax error, unexpected end of file
at /nix/store/37cvj3lklwi0m1ljxs0q8a8v7cc73rcm-source/flake.nix:1:1:$ cat flake.nix ; nix develop
{}
error: flake 'path:/my/repo' lacks attribute 'outputs'$ cat flake.nix ; nix develop
{
outputs = {};
}
error: expected a function but got a set at /nix/store/7nhv5laaqxpwp5ajplyb0dxn3j6wwdyl-source/flake.nix:2:3$ cat flake.nix ; nix develop
{
outputs = _: {};
}
error: flake 'path:/my/repo' does not provide attribute 'devShells.x86_64-linux.default', 'devShell.x86_64-linux', 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux'$ cat flake.nix ; nix develop
{
outputs = _: {
devShells.x86_64-linux.default = {};
};
}
error: expected flake output attribute 'devShells.x86_64-linux.default' to be a derivation or path but found a set: { }$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
}: {
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell
{};
};
}
❄ $Tadaa! Now, we have a dev shell.
To create a dev shell, the mkShell
function available from the Nixpkgs is used.
But there are no dependencies yet.
❄ $ python3 -c 'import sqlalchemy'
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'sqlalchemy'mkShell is a function accepting a single attribute set.
In that attribute set, it is expected that the attribute with the name
packages is a list of derivations to be added to the dev
shell. We add ruff executable and mypy and
SQLAlchemy Python3 packages dependencies.
}: {
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell
- {};
+ {
+ packages = [
+ nixpkgs.legacyPackages.x86_64-linux.ruff
+ nixpkgs.legacyPackages.x86_64-linux.python3Packages.mypy
+ nixpkgs.legacyPackages.x86_64-linux.python3Packages.sqlalchemy
+ ];
+ };
};
}
+❄ $ python3 -c 'import sqlalchemy'
❄ $$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
}: {
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
packages = [
nixpkgs.legacyPackages.x86_64-linux.ruff
nixpkgs.legacyPackages.x86_64-linux.python3Packages.mypy
nixpkgs.legacyPackages.x86_64-linux.python3Packages.sqlalchemy
];
};
};
}
❄ $ python3 -c 'import sqlalchemy'
❄ $The above is platform-dependent solution for creating a dev shell
using Nix flake. We use flake-utils,
particularly eachDefaultSystem and
filterPackages, to support multiple platforms, but you may
not need flake-utils
for that.
eachDefaultSystem is a function that accepts a single
argument. That argument is a function (dev-shell-for in the
following code) that accepts a single argument that is a string
representing a platform and returns an attribute set.
That attribute set is special in that its attributes do not need
x86_64-linux, because eachDefaultSystem will
put each default system at its place instead. Moreover,
system represents the string with the actual each
platform and because dev-shell-for returns attribute set,
we can happily use ${system} where platform is needed.
outputs = {
self,
nixpkgs,
- }: {
- devShells.x86_64-linux.default =
- nixpkgs.legacyPackages.x86_64-linux.mkShell
+ flake-utils,
+ }: let
+ inherit (flake-utils.lib) eachDefaultSystem;
+ dev-shell-for = system:
{
- packages = [
- nixpkgs.legacyPackages.x86_64-linux.ruff
- nixpkgs.legacyPackages.x86_64-linux.python3Packages.mypy
- nixpkgs.legacyPackages.x86_64-linux.python3Packages.sqlalchemy
- ];
+ devShells.default =
+ nixpkgs.legacyPackages.${system}.mkShell
+ {
+ packages = [
+ nixpkgs.legacyPackages.${system}.ruff
+ nixpkgs.legacyPackages.${system}.python3Packages.mypy
+ nixpkgs.legacyPackages.${system}.python3Packages.sqlalchemy
+ ];
+ };
};
- };
+ in
+ eachDefaultSystem dev-shell-for;
}
❄ $$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (flake-utils.lib) eachDefaultSystem;
dev-shell-for = system:
{
devShells.default =
nixpkgs.legacyPackages.${system}.mkShell
{
packages = [
nixpkgs.legacyPackages.${system}.ruff
nixpkgs.legacyPackages.${system}.python3Packages.mypy
nixpkgs.legacyPackages.${system}.python3Packages.sqlalchemy
];
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $Why ${system} is sometimes used and sometimes not? Well…
eachDefaultSystem function takes the output of its argument
(which is a function returning an attribute set) and in that output
attribute set automatically adds ${system} to its
arguments. It means that
let
dev-shell-for = system:
{
devShells.default = ...
};
in
eachDefaultSystem dev-shell-forAdding new dev-shell-for function makes the code more
readable but takes more lines. Experienced Nix buddies may write
something like this instead:
eachDefaultSystem (system: {
devShells.default = ...
})evaluates to
{
devShells.aarch64-linux.default = ...
devShells.aarch64-darwin.default = ...
devShells.x86_64-darwin.default = ...
devShells.x86_64-linux.default = ...
}because "aarch64-linux", "aarch64-darwin",
"x86_64-darwin", and "x86_64-linux" are
default systems by default.
On the other hand, eachDefaultSystem does not manipulate
the values of the attributes. In other words, it keeps what is after
= and before ; and the ${system}
is needed there to make
let
dev-shell-for = system:
{
devShells.default = ... ${system} ...
};
in
eachDefaultSystem dev-shell-forbe evaluated to
{
devShells.aarch64-linux.default = ... aarch64-linux ...
devShells.aarch64-darwin.default = ... aarch64-darwin ...
devShells.x86_64-darwin.default = ... x86_64-darwin ...
devShells.x86_64-linux.default = ... x86_64-linux ...
}filterPackages is a function that accepts two arguments,
a string representing a platform and an attribute set with packages to
by filtered out based on the platform. It returns the filtered attribute
set.
nixpkgs,
flake-utils,
}: let
- inherit (flake-utils.lib) eachDefaultSystem;
+ inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system:
{
- devShells.default =
- nixpkgs.legacyPackages.${system}.mkShell
+ devShells =
+ filterPackages
+ system
{
- packages = [
- nixpkgs.legacyPackages.${system}.ruff
- nixpkgs.legacyPackages.${system}.python3Packages.mypy
- nixpkgs.legacyPackages.${system}.python3Packages.sqlalchemy
- ];
+ default =
+ nixpkgs.legacyPackages.${system}.mkShell
+ {
+ packages = [
+ nixpkgs.legacyPackages.${system}.ruff
+ nixpkgs.legacyPackages.${system}.python3Packages.mypy
+ nixpkgs.legacyPackages.${system}.python3Packages.sqlalchemy
+ ];
+ };
};
};
in$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system:
{
devShells =
filterPackages
system
{
default =
nixpkgs.legacyPackages.${system}.mkShell
{
packages = [
nixpkgs.legacyPackages.${system}.ruff
nixpkgs.legacyPackages.${system}.python3Packages.mypy
nixpkgs.legacyPackages.${system}.python3Packages.sqlalchemy
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $When everything is evaluated on x86_64-linux platform,
there is devShells.x86_64-linux.default dev shell that
includes ruff, mypy, and
sqlalchemy packages if those are available for
x86_64-linux platform.
Recall that the two-arguments function is in fact a single-argument function that returns another single-argument function because currying. Also, the attribute set with packages is an attribute set where attribute names are strings representing package names and the values are derivations.
(A small question for reader: When ; is used solely to
finish = or with or inherit, what
is finished by the ; just after
eachDefaultSystem dev-shell-for? “+ see all” shows the
whole code.)
We are finally happy to have the Nix flake for development. Now it’s time for conventions and refactoring.
By convention, nixpkgs.legacyPackages.${system} is let
to be pkgs and we can shorten code by using
with. This is why we have nixpkgs,
packages, and pkgs with different meanings in
one place. This is confusing.
Also, there are formatting conventions; we use alejandra
and all code snippets here conform to the nix fmt .
command. However, we restrain from shortening the code just to save some
lines.
flake-utils,
}: let
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
- dev-shell-for = system:
- {
- devShells =
- filterPackages
- system
- {
- default =
- nixpkgs.legacyPackages.${system}.mkShell
- {
- packages = [
- nixpkgs.legacyPackages.${system}.ruff
- nixpkgs.legacyPackages.${system}.python3Packages.mypy
- nixpkgs.legacyPackages.${system}.python3Packages.sqlalchemy
- ];
- };
- };
- };
+ dev-shell-for = system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in {
+ formatter = pkgs.alejandra;
+ devShells =
+ filterPackages
+ system
+ {
+ default =
+ pkgs.mkShell
+ {
+ packages = with pkgs; [
+ ruff
+ python3Packages.mypy
+ python3Packages.sqlalchemy
+ ];
+ };
+ };
+ };
in
eachDefaultSystem dev-shell-for;
}$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
python3Packages.mypy
python3Packages.sqlalchemy
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $Instead of enumerating Python3 packages with
python3Packages. prefix, we use
python3.withPackages function.
python3.withPackages function accepts a single argument
that is a function (wanted-py-pkgs in the following code)
that is given the attribute set with all Python3 packages available and
is expected to return the list of wanted packages or – to be more
precise – the list of wanted derivations.
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
+ wanted-py-pkgs = available-pkgs:
+ with available-pkgs; [
+ mypy
+ sqlalchemy
+ ];
in {
formatter = pkgs.alejandra;
devShells =
@@ -19,8 +24,7 @@
{
packages = with pkgs; [
ruff
- python3Packages.mypy
- python3Packages.sqlalchemy
+ (python3.withPackages wanted-py-pkgs)
];
};
};$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = available-pkgs:
with available-pkgs; [
mypy
sqlalchemy
];
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages wanted-py-pkgs)
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $By introducing wanted-py-pkgs we moved the list of
wanted Python3 packages apart from other wanted packages, which is bad
practice. This problem is usually solved by defining function in place
instead:
{
packages = with pkgs; [
ruff
- python3Packages.mypy
- python3Packages.sqlalchemy
+ (python3.withPackages (ps: with ps; [
+ mypy
+ sqlalchemy
+ ]))
];
};
};$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages (ps: with ps; [
mypy
sqlalchemy
]))
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $which looks nice and clean when it’s written but not when read.
To keep wanted-py-pkgs function but also keep wanted
Python3 packages with others, we define wanted-py-pkgs to
accept two arguments and partially
apply the function, i.e., apply the two-argument function to single
argument only.
Recall that ( ) is only used to specify precedence.
nixpkgs,
flake-utils,
}: let
+ inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
+ wanted-py-pkgs = wanted-list: available-pkgs:
+ attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
@@ -19,8 +22,12 @@
{
packages = with pkgs; [
ruff
- python3Packages.mypy
- python3Packages.sqlalchemy
+ (python3.withPackages
+ (wanted-py-pkgs
+ [
+ "mypy"
+ "sqlalchemy"
+ ]))
];
};
};$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
[
"mypy"
"sqlalchemy"
]))
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $Probably the last thing is that the dependencies of a Python3 project
are used to be specified in the pyproject.toml file. (At
least we use pyproject.toml.) Relevant part of the
pyproject.toml may look like:
dependencies = [
"SQLAlchemy>=2.0",
"alembic",
]
which represents a list of package names available from PyPI.
The summary of dependencies for the development is currently:
ruff executable from the Nixpkgs, mypy Python3
package that is not a dependency of the Python3 project but it’s needed
for the development, alembic that is the dependency of the
project, and SQLAlchemy that is also the dependency and
moreover its PyPI name mismatch its name in the Nixpkgs.
Along with the project’s dependencies, there are also optional dependencies for test and docs.
So:
We can use the trivial.importTOML
function to read the TOML file.
We want to support multiple lists of dependencies we would like
to put into the wanted-py-pkgs function.
We want “static” list like in the case of mypy and also
“dynamic” lists taken from the pyproject.toml
file.
We need to “translate” the PyPI package name to the Nixpkgs name when appropriate, i.e., when the package names mismatch.
In the following code, we define translate-py-names
function to translate the list of PyPI names to Nix names for the given
list of Python3 package names and then define and use dependencies from
the pyproject.toml file.
nixpkgs,
flake-utils,
}: let
+ inherit (builtins) match head;
+ inherit (nixpkgs.lib) trivial;
+ proj-toml = trivial.importTOML ./pyproject.toml;
+ translate-py-names = wanted-list: let
+ pypi-name-to-nix-name = {
+ # "pypi-name" = "nix-name";
+ SQLAlchemy = "sqlalchemy";
+ types-PyYAML = "types-pyyaml";
+ };
+ to-nix-name = pypi-name: let
+ pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
+ in
+ pypi-name-to-nix-name.${pkg-name} or pkg-name;
+ in
+ map to-nix-name wanted-list;
+ proj-deps = translate-py-names proj-toml.project.dependencies;
+ test-deps = translate-py-names proj-toml.project.optional-dependencies.test;
+ docs-deps = translate-py-names proj-toml.project.optional-dependencies.docs;
+
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
@@ -24,10 +43,14 @@
ruff
(python3.withPackages
(wanted-py-pkgs
- [
- "mypy"
- "sqlalchemy"
- ]))
+ (
+ [
+ "mypy"
+ ]
+ ++ proj-deps
+ ++ test-deps
+ ++ docs-deps
+ )))
];
};
};$ cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
test-deps = translate-py-names proj-toml.project.optional-dependencies.test;
docs-deps = translate-py-names proj-toml.project.optional-dependencies.docs;
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
++ test-deps
++ docs-deps
)))
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
❄ $This is (the idea behind) our Nix flake for platform-independent develepment environment.
We want all dependencies for development ready when we type
nix develop
and this is how we achieve that:
We use Nix flake – a directory with flake.nix
file.
We need outputs in the flake.nix, it’s
required attribute.
The outputs is a function that accepts at least
self. The outputs function is applied to an
attribute set containing self and then evaluated. The
result of the evaluation is the self.
There can be more attributes in the input attribute set like
nixpkgs or flake-utils. These are well-known
and we use library functions from both and packages from the
first.
The result of the outputs function is required to be
an attribute set. To make nix develop work, that attribute
set needs to contain devShells.${system}.default, where
system is a string with platform (like
"x86_64-linux".)
We make the flake platform-independent by using functions from
flake-utils.
Python3 project dependencies are in different file than
flake.nix, so we import that file and load dependencies
from it.
Moreover, the names of the dependencies are names used in PyPI. These names may mismatch and we need to translate these names to the names used in Nixpkgs.
There are things we want to share between our projects, like Python3 packages. We also want to run applications we develop.
We want building of packages and running applications as easy as
making dev shells – just nix build or nix run,
respectively. This is about building and running a Python3 project. We
start with the flake from the previous section, dropping test and docs
dependencies.
-$ cat flake.nix ; nix develop
+$ cat flake.nix ; nix build
{
outputs = {
self,
@@ -21,8 +21,6 @@
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
- test-deps = translate-py-names proj-toml.project.optional-dependencies.test;
- docs-deps = translate-py-names proj-toml.project.optional-dependencies.docs;
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
@@ -48,8 +46,6 @@
"mypy"
]
++ proj-deps
- ++ test-deps
- ++ docs-deps
)))
];
};
@@ -58,4 +54,4 @@
in
eachDefaultSystem dev-shell-for;
}
-❄ $
+error: flake 'path:/my/repo' does not provide attribute 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux'$ cat flake.nix ; nix build
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
};
in
eachDefaultSystem dev-shell-for;
}
error: flake 'path:/my/repo' does not provide attribute 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux'To build a package, the attribute set returned by the
outputs function of the flake.nix needs to
contain packages.x86_64-linux.default. We already know how
to bypass platform-dependency – using dev-shell-for and
eachDefaultSystem functions.
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
- dev-shell-for = system: let
+ dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
@@ -50,8 +50,9 @@
];
};
};
+ packages.default = {};
};
in
- eachDefaultSystem dev-shell-for;
+ eachDefaultSystem dev-shell-and-packages-for;
}
-error: flake 'path:/my/repo' does not provide attribute 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux'
+error: expected flake output attribute 'packages.x86_64-linux.default' to be a derivation or path but found a set: { }$ cat flake.nix ; nix build
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
packages.default = {};
};
in
eachDefaultSystem dev-shell-and-packages-for;
}
error: expected flake output attribute 'packages.x86_64-linux.default' to be a derivation or path but found a set: { }To build a package, we use callPackage
function; see the origin
of callPackage.
The callPackage function accepts two arguments – a
function and an attribute set. The function (build-pp in
the following code) is given a single attribute set by the
callPackage – the attribute set with filtered Nixpkgs
packages that are needed by the build-pp function.
callPackage knows what packages from Nixpkgs
build-pp needs by investigating the names of the attributes
of the build-pp’s parameter.
The second argument for the callPackage is used to
override Nixpkgs packages and is usually { } in our
case.
The build-pp function is in fact just wrapper that sets
up the information about the package to be built for
python3Packages’s buildPythonPackage
function.
The documentation doesn’t look convincing, but if interested, see buildPythonPackage
function, developing
with Python, and callPackage
tutorial.
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
+ build-pp = {python3Packages}:
+ python3Packages.buildPythonPackage
+ {
+ pname = proj-toml.project.name;
+ inherit (proj-toml.project) version;
+ src = ./.;
+ pyproject = true;
+ build-system = [python3Packages.setuptools];
+ dependencies = attrValues (getAttrs proj-deps python3Packages);
+ };
+
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
@@ -50,9 +61,11 @@
];
};
};
- packages.default = {};
+ packages.default = pkgs.callPackage build-pp {};
};
in
eachDefaultSystem dev-shell-and-packages-for;
}
-error: expected flake output attribute 'packages.x86_64-linux.default' to be a derivation or path but found a set: { }
+$ ./result/bin/run
+foo
+$$ cat flake.nix ; nix build
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
build-pp = {python3Packages}:
python3Packages.buildPythonPackage
{
pname = proj-toml.project.name;
inherit (proj-toml.project) version;
src = ./.;
pyproject = true;
build-system = [python3Packages.setuptools];
dependencies = attrValues (getAttrs proj-deps python3Packages);
};
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
packages.default = pkgs.callPackage build-pp {};
};
in
eachDefaultSystem dev-shell-and-packages-for;
}
$ ./result/bin/run
foo
$Needed to say that for successful build yet we need:
$ cat pyproject.toml
[project]
name = "pp"
version = "0.0.0"
dependencies = [
"SQLAlchemy>=2.0",
"alembic",
]
[project.scripts]
run = "pp.__init__:main"
along with
$ cat pp/__init__.py
def main():
print("foo")
Possible is a little change to use nix run instead of
nix build:
-$ cat flake.nix ; nix build
+$ cat flake.nix ; nix run
{
outputs = {
self,
@@ -62,10 +62,13 @@
};
};
packages.default = pkgs.callPackage build-pp {};
+ apps.default = {
+ type = "app";
+ program = "${self.packages.${system}.default}/bin/run";
+ };
};
in
eachDefaultSystem dev-shell-and-packages-for;
}
-$ ./result/bin/run
foo
$$ cat flake.nix ; nix run
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
build-pp = {python3Packages}:
python3Packages.buildPythonPackage
{
pname = proj-toml.project.name;
inherit (proj-toml.project) version;
src = ./.;
pyproject = true;
build-system = [python3Packages.setuptools];
dependencies = attrValues (getAttrs proj-deps python3Packages);
};
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
packages.default = pkgs.callPackage build-pp {};
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/run";
};
};
in
eachDefaultSystem dev-shell-and-packages-for;
}
foo
$We had to add apps into the attribute set, the output of
the outputs function, because the executable
(run) has different name than the package
(pp).
In the apps.default attribute set, the value of the
program attribute says something like: “Take the result of
the evaluation of the application of the outputs,
self, check its default package of
packages for the system – it should produce a
/bin/run executable – and that’s what should be executed on
nix run.
The following is small example of how to use the
/my/repo Nix flake somewhere else, e.g., in the
/my/dep-rep Nix flake:
$ cd /my/dep-rep/ ; cat flake.nix ; nix run
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
repo,
}: {
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
};
}
foo
$In the code above, there is a new flake /my/dep-rep. In
its flake.nix there is the inputs attribute
set along with the outputs.
inputs specifies where Nix build tool commands (like
nix run) should look for unknown inputs to the
outputs function. self is known but
/my/repo is not.
outputs returns an platform-specific attribute set.
apps is used by the nix run command the same
way the packages is used by the nix build and
devShells by the nix develop.
The default app is run by
nix run. The program being run is what is returned by the
/my/repo Nix flake. Particularly, there is
packages attribute in the /my/repo Nix flake,
which contains the "x86_64-linux" platform with the
default attribute. That default attribute is
something that, when evaluated, creates bin/run
executable.
We can’t be happy yet. Sure we can nix build and
nix run things but what if we want to develop?
-$ cat flake.nix ; nix run
+$ cd /my/repo/ ; cat flake.nix ; nix develop
{
outputs = {
self,
@@ -70,5 +70,4 @@
in
eachDefaultSystem dev-shell-and-packages-for;
}
-foo
-$
+❄ $ python3 -c 'import pp'
+❄ $$ cd /my/repo/ ; cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
build-pp = {python3Packages}:
python3Packages.buildPythonPackage
{
pname = proj-toml.project.name;
inherit (proj-toml.project) version;
src = ./.;
pyproject = true;
build-system = [python3Packages.setuptools];
dependencies = attrValues (getAttrs proj-deps python3Packages);
};
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system};
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
packages.default = pkgs.callPackage build-pp {};
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/run";
};
};
in
eachDefaultSystem dev-shell-and-packages-for;
}
❄ $ python3 -c 'import pp'
❄ $However,
-$ cd /my/dep-rep/ ; cat flake.nix ; nix run
+$ cd /my/dep-rep/ ; cat flake.nix ; nix develop
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
+ nixpkgs,
repo,
}: {
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
+ devShells.x86_64-linux.default =
+ nixpkgs.legacyPackages.x86_64-linux.mkShell
+ {
+ packages = with nixpkgs.legacyPackages.x86_64-linux; [
+ ruff
+ python3Packages.mypy
+ ];
+ };
};
}
-$
+❄ $ python3 -c 'import pp'
+Traceback (most recent call last):
+ File "<string>", line 1, in <module>
+ModuleNotFoundError: No module named 'pp'
+❄ $$ cd /my/dep-rep/ ; cat flake.nix ; nix develop
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
repo,
}: {
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
packages = with nixpkgs.legacyPackages.x86_64-linux; [
ruff
python3Packages.mypy
];
};
};
}
❄ $ python3 -c 'import pp'
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'pp'
❄ $We already know how to create dev shell and install packages there.
Both, the packages directly from Nixpkgs and Python3 packages. But where
to put pp, the Python3 package from /my/repo
Nix flake?
packages = with nixpkgs.legacyPackages.x86_64-linux; [
ruff
python3Packages.mypy
+ python3Packages.pp
];
};
};
}
-❄ $ python3 -c 'import pp'
-Traceback (most recent call last):
- File "<string>", line 1, in <module>
-ModuleNotFoundError: No module named 'pp'
-❄ $
+error:
+ … while calling the 'derivationStrict' builtin
+ at <nix/derivation-internal.nix>:34:12:
+ 33|
+ 34| strict = derivationStrict drvAttrs;
+ | ^
+ 35|
+
+ … while evaluating derivation 'nix-shell'
+ whose name attribute is located at /nix/store/5bshdizjcs9agk532fy4373kl7gbn9cr-source/pkgs/stdenv/generic/make-derivation.nix:375:7
+
+ … while evaluating attribute 'nativeBuildInputs' of derivation 'nix-shell'
+ at /nix/store/5bshdizjcs9agk532fy4373kl7gbn9cr-source/pkgs/stdenv/generic/make-derivation.nix:419:7:
+ 418| depsBuildBuild = elemAt (elemAt dependencies 0) 0;
+ 419| nativeBuildInputs = elemAt (elemAt dependencies 0) 1;
+ | ^
+ 420| depsBuildTarget = elemAt (elemAt dependencies 0) 2;
+
+ (stack trace truncated; use '--show-trace' to show the full, detailed trace)
+
+ error: attribute 'pp' missing
+ at /nix/store/hrslhnqzaikznlkyzvx6c98y8kp1k4il-source/flake.nix:20:11:
+ 19| python3Packages.mypy
+ 20| python3Packages.pp
+ | ^
+ 21| ];
+ Did you mean one of pip, pq, py, zpp or av?
+$$ cd /my/dep-rep/ ; cat flake.nix ; nix develop
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
repo,
}: {
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
packages = with nixpkgs.legacyPackages.x86_64-linux; [
ruff
python3Packages.mypy
python3Packages.pp
];
};
};
}
error:
… while calling the 'derivationStrict' builtin
at <nix/derivation-internal.nix>:34:12:
33|
34| strict = derivationStrict drvAttrs;
| ^
35|
… while evaluating derivation 'nix-shell'
whose name attribute is located at /nix/store/5bshdizjcs9agk532fy4373kl7gbn9cr-source/pkgs/stdenv/generic/make-derivation.nix:375:7
… while evaluating attribute 'nativeBuildInputs' of derivation 'nix-shell'
at /nix/store/5bshdizjcs9agk532fy4373kl7gbn9cr-source/pkgs/stdenv/generic/make-derivation.nix:419:7:
418| depsBuildBuild = elemAt (elemAt dependencies 0) 0;
419| nativeBuildInputs = elemAt (elemAt dependencies 0) 1;
| ^
420| depsBuildTarget = elemAt (elemAt dependencies 0) 2;
(stack trace truncated; use '--show-trace' to show the full, detailed trace)
error: attribute 'pp' missing
at /nix/store/hrslhnqzaikznlkyzvx6c98y8kp1k4il-source/flake.nix:20:11:
19| python3Packages.mypy
20| python3Packages.pp
| ^
21| ];
Did you mean one of pip, pq, py, zpp or av?
$Conceptually, we want /my/repo Nix flake to improve
Nixpkgs by adding new Python3 package. Nixpkg’s overlays are
used for this purpose.
An overlay is a function accepting two arguments,
final and prev, both being attribute set
containing packages (names and derivations). Overlays are put on top of
each other starting at nixpkgs. prev denotes
packages (attribute set) the actual overlay is applied on (i.e., the
result of the application of the previous overlay) and
final is the result attribute set of all overlays
applied.
This is confusing. How could the input parameter to the function,
final, be the result of the evaluation of this and every
other overlay function? Recall that the Nix language is declarative. We
can’t say how, we only say what. How is the problem of
Nix build tool.
By Nix flakes convention, the result of the outputs
function from flake.nix file, the attribute set, may
contain overlays, which is an attribute set containing
overlays. overlays is similar to devShells and
packages, but it is platform-independent – while
devShells.x86_64-linux.default is necessary and we use
eachDefaultSystem to support multiple platforms,
overlays.default is perfectly fine and it is an error to
put overlays in the (attribute set) result of the
dev-shell-and-packages-for function.
In the following code, platform-independent-outputs is
the name used for the attribute set with platform-independent outputs
like overlays. Then, the // operator is used
to augment platform-independent outputs by the platform-dependent ones
and produce the end result of the evaluation of the outputs
function from flake.nix.
Overlays are used to modify the whole nixpkgs attribute
set. However, addition of new Python3 package only affects the
python3Packages attribute of that set – the use case for
the pythonPackagesExtensions.
pythonPackagesExtensions is an attribute of the
attribute set returned by an overlay (overlay is function). It is a list
with overlays that are applied to the “all Python3 package set”
python3Packages at once.
So, to augment nixpkgs with new Python3 package, i.e.,
to add pp into the nixpkgs’s
python3Packages attribute set, the following is needed: (1)
Create new overlay with new package (new-py-pkgs in the
following code) to augment python3Packages, (2) create new
overlay with the pythonPackagesExtensions attribute
(extend-py-extensions in the following code) to augment
nixpkgs and append our new-py-pkgs to that
attribute, (3) set the default of the
overlays. It could be that
default = extend-py-extensions but we get ready for
potential need to compose additional overlays from
inputs.
Feel free to study overlays
and pythonPackagesExtensions.
The code for the /my/repo Nix flake stored in its
flake.nix is now:
@@ -36,7 +36,7 @@
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
- pkgs = nixpkgs.legacyPackages.${system};
+ pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default;
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
@@ -61,13 +61,35 @@
];
};
};
- packages.default = pkgs.callPackage build-pp {};
+ packages.default = pkgs.python3Packages.pp;
+ legacyPackages = pkgs;
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/run";
};
};
+
+ inherit (nixpkgs.lib) composeManyExtensions;
+ platform-independent-outputs = {
+ overlays = {
+ new-py-pkgs = final: prev: {
+ pp = final.callPackage build-pp {};
+ };
+ extend-py-extensions = final: prev: {
+ pythonPackagesExtensions =
+ prev.pythonPackagesExtensions
+ ++ [self.overlays.new-py-pkgs];
+ };
+ default =
+ composeManyExtensions
+ [
+ # Put here overlays from the inputs if appropriate.
+ self.overlays.extend-py-extensions
+ ];
+ };
+ };
in
- eachDefaultSystem dev-shell-and-packages-for;
+ platform-independent-outputs
+ // eachDefaultSystem dev-shell-and-packages-for;
}
❄ $$ cd /my/repo/ ; cat flake.nix ; nix develop
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
build-pp = {python3Packages}:
python3Packages.buildPythonPackage
{
pname = proj-toml.project.name;
inherit (proj-toml.project) version;
src = ./.;
pyproject = true;
build-system = [python3Packages.setuptools];
dependencies = attrValues (getAttrs proj-deps python3Packages);
};
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default;
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
packages.default = pkgs.python3Packages.pp;
legacyPackages = pkgs;
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/run";
};
};
inherit (nixpkgs.lib) composeManyExtensions;
platform-independent-outputs = {
overlays = {
new-py-pkgs = final: prev: {
pp = final.callPackage build-pp {};
};
extend-py-extensions = final: prev: {
pythonPackagesExtensions =
prev.pythonPackagesExtensions
++ [self.overlays.new-py-pkgs];
};
default =
composeManyExtensions
[
# Put here overlays from the inputs if appropriate.
self.overlays.extend-py-extensions
];
};
};
in
platform-independent-outputs
// eachDefaultSystem dev-shell-and-packages-for;
}
❄ $The overlays are functions. We named new-py-pkgs an
overlay we are going to apply to the python3Packages.
There is another overlay, extend-py-extensions, that is
going to be applied to the nixpkgs.
The extend-py-extensions overlay contains
pythonPackagesExtensions – the list of overlays to be
applied to the python3Packages – that we extend by our
new-py-pkgs overlay.
The last overlay we name is default. The
default overlay is composed of many other overlays,
currently a single one, extend-py-extensions. (Not too
many, indeed.)
The default overlay, composed of the
extend-py-extensions overlay, is to be applied to the
nixpkgs, which results to the extension of the
pythonPackagesExtensions by the new-py-pkgs
overlay, and consequentely to the application of the
new-py-pkgs overlay to the python3Packages
with all the other pythonPackagesExtensions overlays at
once.
If lost in overlays, please, keep in mind that there are overlays for
nixpkgs and overlays for python3Packages and
those are different.
We extend the pkgs, convention name for the
nixpkgs for the given system platform, with
our default overlay. Therefore, it is possible to write
pkgs.python3Packages.pp instead of calling the
callPackage function again – pp is already in
the python3Packages.
By convention, we name the legacyPackages. It’s used in
/my/dep-rep Nix flake.
Interesting is how we reference overlays in the overlays
attribute set. { } is a data structure. Writing
new-py-pkgs = and then extend-by-extensions =
do not bring them in the scope of each other. It’s like JSON. It’s not possible to
reference some JSON attribute in another JSON attribute. Neither it’s
possible with Nix language’s attribute sets. However, we have
self! The result of the evaluation of the call to the
outputs function that is given as part of the input to that
function. And self contains overlays that
contains new-py-pkgs, extend-by-extensions,
and default.
What about /my/dep-rep Nix flake?
@@ -18,9 +18,10 @@
packages = with nixpkgs.legacyPackages.x86_64-linux; [
ruff
python3Packages.mypy
- python3Packages.pp
+ repo.legacyPackages.x86_64-linux.python3Packages.pp
];
};
};
}
+❄ $ python3 -c 'import pp'
❄ $$ cd /my/dep-rep/ ; cat flake.nix ; nix develop
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
repo,
}: {
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
packages = with nixpkgs.legacyPackages.x86_64-linux; [
ruff
python3Packages.mypy
repo.legacyPackages.x86_64-linux.python3Packages.pp
];
};
};
}
❄ $ python3 -c 'import pp'
❄ $Looks like the pp Python3 package is there now.
We may use the overlays.default from the
/my/repo input along with the pkgs convention
– we extend the platform-dependent pkgs by the
repo.overlays.default:
@@ -12,13 +12,15 @@
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
- devShells.x86_64-linux.default =
+ devShells.x86_64-linux.default = let
+ pkgs = nixpkgs.legacyPackages.x86_64-linux.extend repo.overlays.default;
+ in
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
- packages = with nixpkgs.legacyPackages.x86_64-linux; [
+ packages = with pkgs; [
ruff
python3Packages.mypy
- repo.legacyPackages.x86_64-linux.python3Packages.pp
+ python3Packages.pp
];
};
};
$$ cd /my/dep-rep/ ; cat flake.nix ; nix develop
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
repo,
}: {
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
devShells.x86_64-linux.default = let
pkgs = nixpkgs.legacyPackages.x86_64-linux.extend repo.overlays.default;
in
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
packages = with pkgs; [
ruff
python3Packages.mypy
python3Packages.pp
];
};
};
}
❄ $ python3 -c 'import pp'
❄ $And in addition, we would use the overlays in the
/my/dep-rep Nix flake, too, because some day
/my/dep-rep could become dependency for some other Nix
flake.
@@ -7,13 +7,32 @@
self,
nixpkgs,
repo,
- }: {
+ }: let
+ inherit (nixpkgs.lib) composeManyExtensions;
+ in {
+ overlays = {
+ new-py-pkgs = final: prev: {
+ # Currently no new Python3 packages by this Nix flake.
+ };
+ extend-py-extensions = final: prev: {
+ pythonPackagesExtensions =
+ prev.pythonPackagesExtensions
+ ++ [self.overlays.new-py-pkgs];
+ };
+ default = composeManyExtensions
+ [
+ # Put here overlays from the inputs if appropriate.
+ repo.overlays.default
+ self.overlays.extend-py-extensions
+ ];
+ };
+
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
devShells.x86_64-linux.default = let
- pkgs = nixpkgs.legacyPackages.x86_64-linux.extend repo.overlays.default;
+ pkgs = nixpkgs.legacyPackages.x86_64-linux.extend self.overlays.default;
in
nixpkgs.legacyPackages.x86_64-linux.mkShell
{$ cd /my/dep-rep/ ; cat flake.nix ; nix develop
{
inputs = {
repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
repo,
}: let
inherit (nixpkgs.lib) composeManyExtensions;
in {
overlays = {
new-py-pkgs = final: prev: {
# Currently no new Python3 packages by this Nix flake.
};
extend-py-extensions = final: prev: {
pythonPackagesExtensions =
prev.pythonPackagesExtensions
++ [self.overlays.new-py-pkgs];
};
default = composeManyExtensions
[
# Put here overlays from the inputs if appropriate.
repo.overlays.default
self.overlays.extend-py-extensions
];
};
apps.x86_64-linux.default = {
type = "app";
program = "${repo.packages.x86_64-linux.default}/bin/run";
};
devShells.x86_64-linux.default = let
pkgs = nixpkgs.legacyPackages.x86_64-linux.extend self.overlays.default;
in
nixpkgs.legacyPackages.x86_64-linux.mkShell
{
packages = with pkgs; [
ruff
python3Packages.mypy
python3Packages.pp
];
};
};
}
❄ $ python3 -c 'import pp'
❄ $This is (the idea behind) our Nix flake for distributing packages and applications.
We want to build an application when we type
nix build
we want to run an application when we type
nix run
we want our packages to be distributed somewhere where we type
nix develop
and this is how we achieve that:
We use Nix flake – a directory with flake.nix
file.
We need outputs in the flake.nix, it’s
required attribute.
The outputs is a function that accepts at least
self. The outputs function is applied to an
attribute set containing self and then evaluated. The
result of the evaluation is the self.
The result of the outputs function is required to be an
attribute set.
To make nix build work, that attribute set needs to
contain packages.${system}.default.
To make nix run work, that attribute set needs to
contain apps.${system}.default.
We use eachDefaultSystem to make the
outputs of the flake platform-independent –
eachDefaultSystem lets us write
packages.default instead of
packages.${system}.default and apps.default
instead of apps.${system}.default, where
system is a string with platform (like
"x86_64-linux".)
We use buildPythonPackage and
callPackage functions to build our Python3
package.
To distribute new package, we use overlays, which
extend nixpkgs and make new package available in (not only
nix develop of) other Nix flakes. Beware that
overlays is platform-independent and must not be put into
the eachDefaultSystem where packages and
apps are.
We use pythonPackagesExtensions attribute of an
attribute set returned by an overlay to add new Python3 package.
pythonPackagesExtensions is a list of overlays that are
applied to the nixpkgs’s python3Packages at
once.
This is about how we deploy our Python3 application to make it run as
a service in NixOS in QEMU. If we build.toplevel instead of
build.vm, then the results directory contains
NixOS system we would deploy with nix copy wrapped in some
Bash.
This is not about how to install NixOS.
We start with
$ cd /my/dep-os/ ; cat flake.nix ; nix build .#nixosConfigurations.S.config.system.build.vm
{
outputs = {
self,
nixpkgs,
}: let
inherit (nixpkgs.lib) nixosSystem;
in {
nixosConfigurations.S =
nixosSystem
{
system = "x86_64-linux";
modules = [];
};
};
}
evaluation warning: system.stateVersion is not set, defaulting to 25.05. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.
$ tree result
result
0 directories, 0 files
$By convention, nixosConfigurations attribute of the
attribute set returned by the outputs function of the
flake.nix is an attribute set, whose attribute names are
names of the systems (like S in our code) and their
corresponding values are the results of nixosSystem
function applications.
system and modules attributes needs to be
present in the attribute set argument when applying
nixosSystem function. Though, it is not enough to build a
runnable system – even the build succeeds the results
directory is empty.
Therefore, we add the basic-system module with the
minimal configuration:
@@ -5,16 +5,28 @@
nixpkgs,
}: let
inherit (nixpkgs.lib) nixosSystem;
+ basic-system = _: {
+ boot.loader.grub.device = "/dev/sda";
+ fileSystems."/".device = "/dev/sda1";
+ services.sshd.enable = true;
+ users.users.iam.initialPassword = "itsme";
+ users.users.iam.isNormalUser = true;
+ };
in {
nixosConfigurations.S =
nixosSystem
{
system = "x86_64-linux";
- modules = [];
+ modules = [
+ basic-system
+ ];
};
};
}
evaluation warning: system.stateVersion is not set, defaulting to 25.05. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.
$ tree result
result
+├── bin
+│ └── run-nixos-vm -> /nix/store/n0gzj07vpk7fcdfga6dq964gbx000nvk-run-nixos-vm
+└── system -> /nix/store/zl8jrwn78amip4nrbcldqhn1q60j8m00-nixos-system-nixos-25.05.20250318.3549532
-0 directories, 0 files
-$
+3 directories, 1 file
+$ QEMU_NET_OPTS='hostfwd=tcp::2222-:22' ./result/bin/run-nixos-vm$ cd /my/dep-os/ ; cat flake.nix ; nix build .#nixosConfigurations.S.config.system.build.vm
{
outputs = {
self,
nixpkgs,
}: let
inherit (nixpkgs.lib) nixosSystem;
basic-system = _: {
boot.loader.grub.device = "/dev/sda";
fileSystems."/".device = "/dev/sda1";
services.sshd.enable = true;
users.users.iam.initialPassword = "itsme";
users.users.iam.isNormalUser = true;
};
in {
nixosConfigurations.S =
nixosSystem
{
system = "x86_64-linux";
modules = [
basic-system
];
};
};
}
evaluation warning: system.stateVersion is not set, defaulting to 25.05. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.
$ tree result
result
├── bin
│ └── run-nixos-vm -> /nix/store/n0gzj07vpk7fcdfga6dq964gbx000nvk-run-nixos-vm
└── system -> /nix/store/zl8jrwn78amip4nrbcldqhn1q60j8m00-nixos-system-nixos-25.05.20250318.3549532
3 directories, 1 file
$ QEMU_NET_OPTS='hostfwd=tcp::2222-:22' ./result/bin/run-nixos-vmAnd we are happy to see new system loading in QEMU. It is possible to
log in with iam username and itsme password.
It also is possible to connect to new system via ssh.
$ ssh -p 2222 iam@localhost
(iam@localhost) Password:
Last login: Thu Mar 20 08:28:45 2025
[iam@nixos:~]$
Configuration is given by the basic-system
module in the code above.
A module is a function expecting a single attribute set argument. The output of the module function is an attribute set with some part of the final configuration. The final configuration is composed from all modules.
All modules from the modules list of the
nixosSystem’s argument are automatically applied to the
automatically generated attribute set containing the following
attributes:
pkgs which is nixpkgslib which is nixpkgs.libconfig – the final configuration (recall overlay’s
final)options – all options (from all modules) to be
configuredmodulesPath – available in NixOS onlyIf additional arguments are needed, it is possible to put them into
the specialArgs attribute (next to the system
and modules) of the nixosSystem’s input. Then,
nixosSystem will pass the additional arguments to all
modules automatically, too.
A module may looks like:
{
pkgs,
config,
...
}: {
config.boot.loader.grub.device = "/dev/sda";
config.fileSystems."/".device = "/dev/sda1";
config.services.sshd.enable = true;
config.users.users = {
iam = {
initialPassword = "itsme";
isNormalUser = true;
};
};
}The config in the input attribute set is the final
overall configuration. The config in the output attribute
set is partial configuration provided by this module. These two
configs are different.
This is confusing, because it looks like the config is
somehow modified. However, recall that the Nix language is purely
functional. Things can’t be changed.
The same works for options that is not used in the
example above.
... is used because more arguments are passed to the
module function.
If only config is used in the module, it is optional.
The module above is the same as the basic-system module in
previous example.
Along with config and options, the result
attribute set of a module function may contain imports – a
list of modules that will be merged into that module, i.e., will be part
of the output attribute set of the module (module is function.)
This is in nutshell how NixOS module system works.
Now to the serve-pp service; we need a module for it and
then use that module. The question is to which Nix flake put the
configuration to? Both are possible – /my/repo and
/my/dep-os.
In the following code, we: (1) Create module for new systemd service
serve-pp to run executable of pp
Python3 package that is part of /my/repo Nix flake, (2)
enable serve-pp service in the /my/dep-os Nix
flake and (3) configure the new system to run serve-pp
periodically.
The update to the /my/repo Nix flake is more about systemd.service
and how to map it to the NixOS
options:
@@ -87,6 +87,26 @@
self.overlays.extend-py-extensions
];
};
+
+ nixosModules = {
+ serve-pp = {lib, config, pkgs, ...}: let
+ inherit (lib) mkEnableOption mkIf;
+ in {
+ options.services.serve-pp.enable = mkEnableOption "serve-pp";
+ config =
+ mkIf
+ config.services.serve-pp.enable
+ {
+ systemd.services.serve-pp = {
+ wantedBy = ["multi-user.target"];
+ script = "${pkgs.python3Packages.pp}/bin/run";
+ serviceConfig = {
+ Type = "oneshot";
+ };
+ };
+ };
+ };
+ };
};
in
platform-independent-outputs$ cd /my/repo/ ; cat flake.nix ; nix build
{
outputs = {
self,
nixpkgs,
flake-utils,
}: let
inherit (builtins) match head;
inherit (nixpkgs.lib) trivial;
proj-toml = trivial.importTOML ./pyproject.toml;
translate-py-names = wanted-list: let
pypi-name-to-nix-name = {
# "pypi-name" = "nix-name";
SQLAlchemy = "sqlalchemy";
types-PyYAML = "types-pyyaml";
};
to-nix-name = pypi-name: let
pkg-name = head (match "([^ =<>;~]*).*" pypi-name);
in
pypi-name-to-nix-name.${pkg-name} or pkg-name;
in
map to-nix-name wanted-list;
proj-deps = translate-py-names proj-toml.project.dependencies;
build-pp = {python3Packages}:
python3Packages.buildPythonPackage
{
pname = proj-toml.project.name;
inherit (proj-toml.project) version;
src = ./.;
pyproject = true;
build-system = [python3Packages.setuptools];
dependencies = attrValues (getAttrs proj-deps python3Packages);
};
inherit (nixpkgs.lib) attrValues getAttrs;
inherit (flake-utils.lib) eachDefaultSystem filterPackages;
dev-shell-and-packages-for = system: let
pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default;
wanted-py-pkgs = wanted-list: available-pkgs:
attrValues (getAttrs wanted-list available-pkgs);
in {
formatter = pkgs.alejandra;
devShells =
filterPackages
system
{
default =
pkgs.mkShell
{
packages = with pkgs; [
ruff
(python3.withPackages
(wanted-py-pkgs
(
[
"mypy"
]
++ proj-deps
)))
];
};
};
packages.default = pkgs.python3Packages.pp;
legacyPackages = pkgs;
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/run";
};
};
inherit (nixpkgs.lib) composeManyExtensions;
platform-independent-outputs = {
overlays = {
new-py-pkgs = final: prev: {
pp = final.callPackage build-pp {};
};
extend-py-extensions = final: prev: {
pythonPackagesExtensions =
prev.pythonPackagesExtensions
++ [self.overlays.new-py-pkgs];
};
default =
composeManyExtensions
[
# Put here overlays from the inputs if appropriate.
self.overlays.extend-py-extensions
];
};
nixosModules = {
serve-pp = {lib, config, pkgs, ...}: let
inherit (lib) mkEnableOption mkIf;
in {
options.services.serve-pp.enable = mkEnableOption "serve-pp";
config =
mkIf
config.services.serve-pp.enable
{
systemd.services.serve-pp = {
wantedBy = ["multi-user.target"];
script = "${pkgs.python3Packages.pp}/bin/run";
serviceConfig = {
Type = "oneshot";
};
};
};
};
};
};
in
platform-independent-outputs
// eachDefaultSystem dev-shell-and-packages-for;
}
$There is new platform-independent output in /my/repo Nix
flake named nixosModules by convention.
nixosModules is an attribute set with single attribute
named serve-pp; serve-pp is a module.
The output attribute set of the serve-pp module contains
options and config, partial configuration
provided by the serve-pp module.
First
+ options.services.serve-pp.enable = mkEnableOption "serve-pp";says there is an option services.serve-pp.enable that
can be configured either to be true or false.
mkEnableOption
is a shortcut to mkOption.
Then
+ config =
+ mkIf
+ config.services.serve-pp.enable
+ {says that if the final configuration (config) contains
services.serve-pp.enable with value true, it
also contains:
+ systemd.services.serve-pp = {
+ wantedBy = ["multi-user.target"];
+ script = "${pkgs.python3Packages.pp}/bin/run";
+ serviceConfig = {
+ Type = "oneshot";
+ };
+ };
+ };(Nix language is declarative, so we specify relations and say it also contains rather than then add to it.)
Recall that pp is a Python3 package and it’s
run script is specified in its pyproject.toml
file. This is the reason why script = ... looks like it
looks – run is an executable of pp Python3
package.
That was how systemd configuration is made available for other Nix
flakes. The update to the /my/dep-os Nix flake shows how it
can be used:
@@ -1,8 +1,12 @@
$ cd /my/dep-os/ ; cat flake.nix ; nix build .#nixosConfigurations.S.config.system.build.vm
{
+ inputs = {
+ my-repo.url = "path:///my/repo/";
+ };
outputs = {
self,
nixpkgs,
+ my-repo,
}: let
inherit (nixpkgs.lib) nixosSystem;
basic-system = _: {
@@ -11,6 +15,9 @@
services.sshd.enable = true;
users.users.iam.initialPassword = "itsme";
users.users.iam.isNormalUser = true;
+ users.users.iam.group = "wheel"; # for sudo
+ services.serve-pp.enable = true;
+ nixpkgs.overlays = [my-repo.overlays.default];
};
in {
nixosConfigurations.S =
@@ -19,6 +26,7 @@
system = "x86_64-linux";
modules = [
basic-system
+ my-repo.nixosModules.serve-pp
];
};
};$ cd /my/dep-os/ ; cat flake.nix ; nix build .#nixosConfigurations.S.config.system.build.vm
{
inputs = {
my-repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
my-repo,
}: let
inherit (nixpkgs.lib) nixosSystem;
basic-system = _: {
boot.loader.grub.device = "/dev/sda";
fileSystems."/".device = "/dev/sda1";
services.sshd.enable = true;
users.users.iam.initialPassword = "itsme";
users.users.iam.isNormalUser = true;
users.users.iam.group = "wheel"; # for sudo
services.serve-pp.enable = true;
nixpkgs.overlays = [my-repo.overlays.default];
};
in {
nixosConfigurations.S =
nixosSystem
{
system = "x86_64-linux";
modules = [
basic-system
my-repo.nixosModules.serve-pp
];
};
};
}
evaluation warning: system.stateVersion is not set, defaulting to 25.05. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.
$ QEMU_NET_OPTS='hostfwd=tcp::2222-:22' ./result/bin/run-nixos-vmThere, my-repo is new input representing
/my/repo Nix flake. Moreover
+ services.serve-pp.enable = true;says that serve-pp service should be enabled,
+ nixpkgs.overlays = [my-repo.overlays.default];says that nixpkgs should be overlayed by the overlay
from the my-repo (where pp Python3 package is)
and
+ my-repo.nixosModules.serve-ppis there to make new system aware of serve-pp module
that is responsible for making the services.serve-pp.enable
option and services.serve-pp configuration available.
Now it is possible to connect to the system running in QEMU and start
the monitoring of the serve-pp service:
$ ssh -p 2222 iam@localhost
(iam@localhost) Password:
Last login: Thu Mar 20 12:47:56 2025 from 10.0.2.2
[iam@nixos:~]$ journalctl -u serve-pp.service -f
Mar 20 13:53:13 nixos systemd[1]: Starting serve-pp.service...
Mar 20 13:53:17 nixos serve-pp-start[682]: foo
Mar 20 13:53:17 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 13:53:17 nixos systemd[1]: Finished serve-pp.service.
Then, connect to the system in other terminal and run the
serve-pp service:
$ ssh -p 2222 iam@localhost
(iam@localhost) Password:
Last login: Thu Mar 20 13:54:34 2025 from 10.0.2.2
[iam@nixos:~]$ sudo systemctl start serve-pp.service
[sudo] password for iam:
[iam@nixos:~]$
which results in the additional output in the log:
Mar 20 13:57:48 nixos systemd[1]: Starting serve-pp.service...
Mar 20 13:57:48 nixos serve-pp-start[929]: foo
Mar 20 13:57:48 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 13:57:48 nixos systemd[1]: Finished serve-pp.service.
We like to keep our source code reasonably divided
@@ -16,6 +16,8 @@
users.users.iam.initialPassword = "itsme";
users.users.iam.isNormalUser = true;
users.users.iam.group = "wheel"; # for sudo
+ };
+ enable-serve-pp = _: {
services.serve-pp.enable = true;
nixpkgs.overlays = [my-repo.overlays.default];
};
@@ -27,6 +29,7 @@
modules = [
basic-system
my-repo.nixosModules.serve-pp
+ enable-serve-pp
];
};
};$ cd /my/dep-os/ ; cat flake.nix ; nix build .#nixosConfigurations.S.config.system.build.vm
{
inputs = {
my-repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
my-repo,
}: let
inherit (nixpkgs.lib) nixosSystem;
basic-system = _: {
boot.loader.grub.device = "/dev/sda";
fileSystems."/".device = "/dev/sda1";
services.sshd.enable = true;
users.users.iam.initialPassword = "itsme";
users.users.iam.isNormalUser = true;
users.users.iam.group = "wheel"; # for sudo
};
enable-serve-pp = _: {
services.serve-pp.enable = true;
nixpkgs.overlays = [my-repo.overlays.default];
};
in {
nixosConfigurations.S =
nixosSystem
{
system = "x86_64-linux";
modules = [
basic-system
my-repo.nixosModules.serve-pp
enable-serve-pp
];
};
};
}
evaluation warning: system.stateVersion is not set, defaulting to 25.05. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.
$ QEMU_NET_OPTS='hostfwd=tcp::2222-:22' ./result/bin/run-nixos-vmand we promised to run the serve-pp service
periodically
@@ -19,6 +19,7 @@
};
enable-serve-pp = _: {
services.serve-pp.enable = true;
+ systemd.services.serve-pp.startAt = "*:*:0/5";
nixpkgs.overlays = [my-repo.overlays.default];
};
in {$ cd /my/dep-os/ ; cat flake.nix ; nix build .#nixosConfigurations.S.config.system.build.vm
{
inputs = {
my-repo.url = "path:///my/repo/";
};
outputs = {
self,
nixpkgs,
my-repo,
}: let
inherit (nixpkgs.lib) nixosSystem;
basic-system = _: {
boot.loader.grub.device = "/dev/sda";
fileSystems."/".device = "/dev/sda1";
services.sshd.enable = true;
users.users.iam.initialPassword = "itsme";
users.users.iam.isNormalUser = true;
users.users.iam.group = "wheel"; # for sudo
};
enable-serve-pp = _: {
services.serve-pp.enable = true;
systemd.services.serve-pp.startAt = "*:*:0/5";
nixpkgs.overlays = [my-repo.overlays.default];
};
in {
nixosConfigurations.S =
nixosSystem
{
system = "x86_64-linux";
modules = [
basic-system
my-repo.nixosModules.serve-pp
enable-serve-pp
];
};
};
}
evaluation warning: system.stateVersion is not set, defaulting to 25.05. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion.
$ QEMU_NET_OPTS='hostfwd=tcp::2222-:22' ./result/bin/run-nixos-vmThe systemd.services.serve-pp.startAt extends the
configuration. It is not a part of the original module.
Beware of services and systemd services, becase systemd is a service in NixOS that manage other systemd services but not NixOS services.
Also, it looks like the combination of systemd, NixOS and QEMU is not much time-precise.
$ ssh -p 2222 iam@localhost
(iam@localhost) Password:
Last login: Thu Mar 20 14:53:32 2025 from 10.0.2.2
[iam@nixos:~]$ journalctl -u serve-pp -f
Mar 20 14:56:53 nixos systemd[1]: Starting serve-pp.service...
Mar 20 14:56:55 nixos serve-pp-start[683]: foo
Mar 20 14:56:55 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 14:56:55 nixos systemd[1]: Finished serve-pp.service.
Mar 20 14:57:01 nixos systemd[1]: Starting serve-pp.service...
Mar 20 14:57:01 nixos serve-pp-start[896]: foo
Mar 20 14:57:01 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 14:57:01 nixos systemd[1]: Finished serve-pp.service.
Mar 20 14:57:31 nixos systemd[1]: Starting serve-pp.service...
Mar 20 14:57:31 nixos serve-pp-start[926]: foo
Mar 20 14:57:31 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 14:57:31 nixos systemd[1]: Finished serve-pp.service.
Mar 20 14:57:36 nixos systemd[1]: Starting serve-pp.service...
Mar 20 14:57:36 nixos serve-pp-start[929]: foo
Mar 20 14:57:36 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 14:57:36 nixos systemd[1]: Finished serve-pp.service.
Mar 20 14:58:23 nixos systemd[1]: Starting serve-pp.service...
Mar 20 14:58:23 nixos serve-pp-start[932]: foo
Mar 20 14:58:23 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 14:58:23 nixos systemd[1]: Finished serve-pp.service.
Mar 20 14:58:31 nixos systemd[1]: Starting serve-pp.service...
Mar 20 14:58:31 nixos serve-pp-start[935]: foo
Mar 20 14:58:31 nixos systemd[1]: serve-pp.service: Deactivated successfully.
Mar 20 14:58:31 nixos systemd[1]: Finished serve-pp.service.
This is (the idea behind) our Nix flake for services deployment.
We want to build NixOS system with
nix build .#nixosConfigurations.S.config.system.build.vm
where S is a name of new system. Then we want to run it
in QEMU like
QEMU_NET_OPTS='hostfwd=tcp::2222-:22' ./result/bin/run-nixos-vm
When we build .toplevel instead
nix build .#nixosConfigurations.S.config.system.build.toplevel
we deploy to running NixOS with
nix copy
and this is how we achieve that:
We use Nix flakes – directories with flake.nix
files.
We need outputs in the flake.nix, it’s
required attribute.
The outputs is a function that accepts at least
self. The outputs function is applied to an
attribute set containing self and then evaluated. The
result of the evaluation is the self.
The result of the outputs function is required to be an
attribute set.
We add platform-independent attribute to the attribute set
returned by the outputs function. By convention, the name
of the attribute is nixosModules and it contains building
blocks for the configuration of a system.
nixosModules is platform-independent, i.e., we do not
write
nixosModules.x86_64-linux.serve-pp = ...but directly
nixosModules.serve-pp = ...nixosModules is part of the Nix flake that builds and
runs our application and we specify in nixosModules how our
application should be run as systemd service.
We have another Nix flake that manages our systems. We also
extend the attribute set returned by the outputs
function.
By convention, we use nixosConfigurations for the
attribute name. It stores pairs of new system’s name and result of the
nixosSystem function call.
nixosConfigurations is also
platform-independent.
We use Nix with flakes for many things. I don’t say it’s good or bad – it’s a state of things in a company I work.
Nix flake is a directory with flake.nix file. In such a
directory, we run
nix run
to execute an application, which is possible due to
apps.x86_64-linux.default = ...
in flake.nix file of Nix flake (directory.)
x86_64-linux specifies platform and when reading
flake.nix, there is usually ${system} instead,
because we write flake.nix in such way it supports multiple
platforms.
Nix flakes help us with keeping development environment consistent; we can
nix develop
while keeping
devShells.x86_64-linux.default = ...
in flake.nix and be free of “I don’t have this error.
What version of the library you used?” nevermind the programming
language.
But dependencies for development is not all. When the dependencies are assured by Nix flake, why not whole build? We build our application with
nix build
which depends on
packages.x86_64-linux.default = ...
being set in flake.nix.
What does it mean to build an application, anyway? Make it executable, I guess.
Applications have different kinds of dependencies. Some are needed just to build an application, to make the application executable. However, there are dependencies applications need while being executed – runtime dependencies.
We build applications to be executed and it’s good to ask how to execute these applications? What are their runtime dependencies? How to satisfy these dependencies?
Well… Nix knows about all dependencies since the beginning so executing an application built with Nix in NixOS is obvious solution. We keep
nixosConfigurations.our-system = ...
in flake.nix and we can build that system the same way
we build applications, with nix build. Although building a
system is more about creating files to be later nix copy in
order to update/replace another system running NixOS.
Last piece of puzzle is how to share applications and configurations between Nix flakes; we keep
overlays.default = ...
legacyPackages.x86_64-linux = ...
and
nixosModules.our-service = ...
respectively in flake.nix file.
We use import ./path/to/file.nix often but not in
examples above. import evaluates the content of the file,
which may be a function declaration. Therefore, if
$ cat ./file.nix
first: second: first + second
then
import ./file.nix 3 4 == 7
is true.
It looks like we use Nix with flakes since cradle to the grave.
Considering Nix flakes being experimental, that’s scary.
And Nix laungage itself does not help, neither so many conventions and confusions one needs to keep in mind.
A little ❄ when nix develop is by:
$ cat ~/.config/nix/nix.conf
bash-prompt-prefix = ❄\040
go back | CC BY-NC-SA 4.0 Jiri Vlasak