Practical Nix flake anatomy: a guided tour of flake.nix
Published on
Overview of the flake.nix
file, its components and what they are used for.
About this post #
There is a quite widespread opinion that Nix documentation is very fragmented and hard to follow. In case of flakes there’s already a number of published materials that talk about how to get started with them and use them.
What I think is missing is a single-page description of the actual flake.nix
file with answers to “why would I even need this part” that is aimed to help users who are newer to Nix to make flakes that they encounter in the wild less cryptic.
Since flakes are considered experimental, certain things might change after writing. At time of writing the Nix version is 2.21.0.
The code samples are intended to be as self-sufficient as possible and sometimes will be overly verbose on purpose.
Flake definition #
For the purposes of this post let’s treat a flake as a function that takes inputs and produces outputs. The inputs could be any valid references and the outputs are expected1 to follow a schema.
The outputs could be Linux packages, pieces of NixOS configuration or any other artifact written in nix.
Flake attributes #
Every heading in this section is mapped to a top-level attribute of flake.nix
. “Attribute” in this context is used as in nix-the-language – a key in a dictionary.
Description #
Description is (typically) a short blurb about what is the point of the flake.
An example flake.nix
could look like:
{
description = "Some description";
outputs = _: { };
}
For the sample flake above, the output is:
❯ nix flake metadata .
Resolved URL: path:/tmp/tmp.fe06VZ7KtP
Locked URL: path:/tmp/tmp.fe06VZ7KtP?<boring>
Description: Some description
Path: /nix/store/cak4fgj5b3ricl962sqbxs2g2pbc2vyc-source
Last modified: 2024-03-30 <boring>
Contrary to the main doc, it does not have to be one line either but it looks weird in metadata output:
❯ nix flake metadata .
Resolved URL: path:/tmp/tmp.fe06VZ7KtP
Locked URL: path:/tmp/tmp.fe06VZ7KtP?<boring>
Description: Some description.
There is a line break too!
Path: /nix/store/6kd07z6hv04akbncqly3yvdc11wly7nm-source
Last modified: 2024-03-30 <boring>
Logical next question is – is it just a string or can I use nix language to calculate it? Spoiler: no.
{
description = let a = "Some description"; in a;
outputs = _: { };
}
Results in:
❯ nix flake metadata .
error: expected a string but got a thunk at /nix/store/31xvhc041yrh9by1605185sihbr9np60-source/flake.nix:2:3
Since nix cannot be evaluated at the top level of the flake; see nixpkgs#4945.
Strictly speaking, description is not needed. They are present in the output of nix flake metadata
of course or could be used by downstream things like flake registry. Only outputs
attribute is required.
Inputs #
Flakes could be viewed as functions. Functions can accept some inputs to produce something that would depend on the inputs. Flake inputs are references which are pointing to:
- Other flakes
or
- File paths
For the syntax of inputs, see the “Examples” section in the flake reference.
Most of the flakes in the wild take some bag of inputs (nixpkgs
, some package sources, other flakes) and produce packages, NixOS configurations, WTFs per minute, etc. So most likely you would want to define some inputs.
Note that if an input is a well known one and comes from the public registry, it can be omitted in the inputs
attribute.
{
description = "Some description";
# Note that no explicit inputs are provided
outputs = { nixpkgs, self }: { packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.hello; };
}
Will happily produce:
❯ nix flake metadata .
Resolved URL: path:/tmp/tmp.fe06VZ7KtP
Locked URL: path:/tmp/tmp.fe06VZ7KtP?<boring>
Description: Some description
Path: /nix/store/q7hk3cqjp3b5vzbnhnqkpkqaf9liwkbn-source
Last modified: 2024-03-30 17:51:05
Inputs:
└───nixpkgs: github:NixOS/nixpkgs/807c549feabce7eddbf259dbdcec9e0600a0660d?narHash=sha256-9slQ609YqT9bT/MNX9%2B5k5jltL9zgpn36DpFB7TkttM%3D (2024-03-29 12:35:36)
❯ nix run .
Hello, world!
Note that as part of NixOS 24.05 the behavior of
nixpkgs
input has changed. Now it will point to the version of nixpkgs used to
build the system, which will look like this in the metadata:
Inputs:
└───nixpkgs: path:/nix/store/2nnisw4pxbifisbkg58hrnis9ycs5ah1-source?lastModified=0&narHash=sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo%3D (1970-01-01 00:00:00)
This might break running the flake as-is on a system with different copy of nixpkgs.
On the other hand, flake without any inputs at all can still be useful. For example:
{
outputs = _: { my-output = 1 + 1; };
}
❯ nix eval .#my-output
2
inputs.follows #
One important thing to note is that a flake’s consumer can override the flake’s transitive input by setting inputs,<flakeref>.inputs.<transitiveDep>.follows = "<anotherFlakeRef>"
. This is useful if, say, imported flake is pinned to an old version of nixpkgs
but is otherwise perfectly usable.
For example, at time of writing home-manager
depends on nixpkgs
revision d8fe5e6
. I would like it to depend on nixpkgs
revision 807c549
which is the default nixpkgs
version in flake registry at time of writing.
With a simple .follows
, home-manager
in this flake will use the specified version of nixpkgs
:
{
inputs.home-manager.url = "github:nix-community/home-manager";
inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs";
outputs = _: {};
}
Flakes as flake inputs #
By default, an input is a flake reference. For example:
{
description = "Some description";
inputs.nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
outputs = _: {};
}
Then the input can be used in an outputs as a nix attribute set which follows a schema. The flake schema to be precise; case in point, a flake like:
{
description = "Some description";
inputs.nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
outputs =
{ nixpkgs-unstable, ... }: # Outputs' arguments are described in "frequently encountered code patterns"
{
packages.x86_64-linux.default = nixpkgs-unstable.legacyPackages.x86_64-linux.hello;
};
}
Results in:
❯ nix flake metadata .
Resolved URL: path:/tmp/tmp.fe06VZ7KtP
Locked URL: path:/tmp/tmp.fe06VZ7KtP?lastModified=1711844267&narHash=sha256-Z6lN1WenLMaWzHTDzsrKwx/Ty%2BXxYZtuot%2Brp4
ye/A4%3D
Description: Some description
Path: /nix/store/8q8xak5yzbz5bx87xsp4lx83ggsmpc28-source
Last modified: 2024-03-30 17:17:47
Inputs:
└───nixpkgs-unstable: github:NixOS/nixpkgs/d8fe5e6c92d0d190646fb9f1056741a229980089?narHash=sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk%3D (2024-03-29 09:07:56)
❯ nix run .
Hello, world!
Non-flakes as flake inputs #
Flakes can take arbitrary paths as inputs. This is mostly used to bring some package source into the flake and then pass to packages
to produce a Nix package. For example:
{
description = "Some description";
inputs = {
nonFlakeInput = {
url = "path:///tmp/tmp.runi5XRjnC";
flake = false;
};
nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
};
outputs =
{
nonFlakeInput,
nixpkgs-unstable,
self,
}:
let
pkgs = import nixpkgs-unstable { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.default = pkgs.runCommandLocal "example" { } ''
mkdir -p $out/bin
cp ${nonFlakeInput}/example $out/bin # "nonFlakeInput" will translate here to the /nix/store path where nonFlakeInput is placed during evaluation
chmod +x $out/bin/example
'';
};
}
The directory that is passed as an input contains a single file:
#!/usr/bin/env bash
echo "Hello world!"
And this ultimately results in:
❯ nix run .
Hello world!
This way a random nonFlakeInput
can be provided as the source to some language-specific builder to produce a package in that language.
Outputs #
Outputs are arguably the most useful part of the flake. They allow flake’s consumers to use the flake’s contents.
The outputs are defined broadly into two categories:
-
System-specific outputs. In this context “system” denotes the architecture and type of the environment where nix is run: aarch64/x86_64; linux/darwin, etc.
Such outputs often rely on platform specific components. Darwin might not have
systemd
; some packages may only be available for x86_64 architecture and so on. -
System-independent outputs. These outputs may be completely system-independent:
{ outputs = _: { my-output = 1 + 1; }; }
❯ nix eval .#my-output 2
Or may handle dependency on the system in the code. Long-running services when defined in modules may have special provisions for MacOS (*-darwin) that replace systemd services with their launchd analogs.
Outputs-the-attribute in flake.nix
needs to be a function that produces an attribute set. Simplest (but somewhat pointless) example is:
# Yes, this is a complete flake.nix
{ outputs = _: { }; }
And individual outputs are attributes in this set:
{ outputs = _: { my-output = 1 + 1; }; }
Every output is optional. However, a flake without outputs doesn’t have much purpose beyond an exercise in writing code.
System-specific outputs #
packages #
Arguably the single most commonly encountered output of a flake. packages
attribute should contain an attribute set with values being derivations.
For a simple demonstration, let’s use a simple flake that exposes pkgs.hello
as its default package:
{
outputs =
{ nixpkgs, ... }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.default = pkgs.hello;
};
}
Side note: the “default
” allows commands like nix run
to omit specifying the output to run. And without a default
specified for the output, nix flake check
will complain.
These derivations may be used in a variety of ways:
-
Run directly:
# Assuming this is run in the same directory as flake.nix ❯ nix run Hello, world!
-
In a development shell of the same flake (with a minor adjustment to the
flake.nix
of course)# same flake.nix { outputs = { nixpkgs, self }: # Note the use of `self` which allows reusing flake's outputs in itself let pkgs = import nixpkgs { system = "x86_64-linux"; }; in { packages.x86_64-linux.default = pkgs.hello; devShells.x86_64-linux.default = pkgs.mkShell { packages = [ self.packages.x86_64-linux.default ]; }; }; }
❯ which hello hello not found ❯ nix develop # <Development shell activates> ❯ which hello /nix/store/rnxji3jf6fb0nx2v0svdqpj9ml53gyqh-hello-2.12.1/bin/hello
-
Installed by a consumer in a separate flake in a NixOS or a home-manager environment
In a single flake:
# consuming flake.nix { inputs.flake-with-package.url = "path:////tmp/tmp.fe06VZ7KtP"; outputs = { self, nixpkgs, flake-with-package, }: { nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ { environment.systemPackages = [ flake-with-package.packages.x86_64-linux.default ]; } ./configuration.nix # The rest of the system configuration ]; }; }; }
Or in a combination of flakes and modules; flake references can be passed to modules:
# consuming flake.nix { inputs.flake-with-package.url = "path:////tmp/tmp.fe06VZ7KtP"; outputs = { self, nixpkgs, flake-with-package, }: { nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; specialArgs = { inherit flake-with-package; }; modules = [ ./configuration.nix # The rest of the system configuration ]; }; }; } # configuration.nix { flake-with-package, pkgs, ... }: # specialArgs in the call to nixosSystem make flake-with-package available to be used inside the module { environment.systemPackages = [ flake-with-package.packages.${pkgs.system}.default ]; # pkgs.system is a common pattern to not repeat the system architecture fileSystems."/".device = "/dev/sda"; boot.loader.grub.devices = [ "/" ]; }
apps #
Apps are fairly well described in the NixOS manual. They are effectively the primary target of nix run
. Do note that apps need to follow a specific schema:
{
type = "app"; # Needs to be literal "app"
program = "/nix/store/..." # Needs to be a path in store. Common way is to use lib.getExe, lib.getExe' or specify the path to an executable by hand.
}
legacyPackages #
legacyPackages output is very similar to packages
but exists for a performance reason best outlined in nixpkgs
flake.
nix run .#foo
would try to look up “foo” in this order:
apps
packages
legacyPackages
As evident by trying to run nix run .#foo
in a flake like the one below and removing attributes one by one.
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.foo = pkgs.writeShellScriptBin "foo" "echo 'I am a package'";
legacyPackages.x86_64-linux.foo = pkgs.writeShellScriptBin "foo" "echo 'I am a legacyPackage'";
apps.x86_64-linux.foo = {
type = "app";
program = pkgs.lib.getExe (pkgs.writeShellScriptBin "foo" "echo 'I am an app'");
};
};
}
checks #
Checks are mainly executed when one runs nix flake check
. Default behavior is to check the flake against the default schema and emit warnings if non-standard outputs are found or the outputs are not what they are supposed to be. Checks are also derivations and it’s possible to define custom ones. To that end they can be used as integration tests.
Let’s use the flake below as an example. It declares a package and a module as two outputs. The two additional checks packageTest
and moduleTest
are very simple integration tests that validate the output of the package
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.default = pkgs.writeShellScriptBin "foo" "echo -n 'I am a custom package'";
nixosModules.default =
{ pkgs, ... }:
{
environment.systemPackages = [ self.packages.${pkgs.system}.default ];
};
checks.x86_64-linux = {
# If for whatever reason the package definition does not contain integration tests
packageTest = pkgs.stdenv.mkDerivation {
name = "packageTest";
dontUnpack = true;
dontBuild = true;
doCheck = true;
nativeBuildInputs = [ self.packages.x86_64-linux.default ];
checkPhase = ''
if [ "$(foo)" = "I am a custom package" ]; then
echo "OK"
else
echo "NOK"
exit 1
fi
'';
installPhase = "mkdir $out"; # "$out" needs to be produced by a derivation, otherwise check will not pass the check
};
# Check the module
moduleTest = pkgs.testers.runNixOSTest {
name = "moduleTest";
nodes.machine1 = {
imports = [ self.nixosModules.default ];
};
testScript =
# See the devmanual for available python methods:
# https://nixos.org/manual/nixos/stable/#ssec-machine-objects
''
assert machine.execute("foo")[1] == "I am a custom package"
'';
};
};
};
}
Unlike nix run
, nix flake check
does not accept a path to a check; it executes all of them. However, the derivations inside checks
can be executed by nix build
. In the example above:
❯ nix build .#checks.x86_64-linux.packageTest # Equivalent to running a single check
Since runNixOSTest
test a module and modules need the rest of the system, this check builds an actual VM and executes testScript
. This VM can be run in an interactive mode:
❯ nix run .#checks.x86_64-linux.moduleTest.driverInteractive # Note the .driverInteractive part
...
# python console starts
>>> start_all()
which will bring up a interactive QEMU VM window with the VM where flake’s module is used. Note that runNixOSTest
will helpfully fill in the rest of the system; we don’t need to specify things like fileSystems and boot loader.
devShells #
devShell
is an environment that gets activated when nix develop <flakeref>
is run. This is most useful to quickly bring in development-only dependencies that may be needed for work on the source code.
Typically it is constructed as a call to pkgs.mkShell
which is a wrapper around mkDerivation
with extra attributes.
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
nativeBuildInputs = [ pkgs.hello ];
};
};
}
❯ which hello
hello not found
❯ hello
Hello, world!
There are quite a few flake-related tools that allow more flexibility than the standard call to mkShell
:
As with packages
, legacyPackages
and apps
, there can be a default
devShell with gets executed by, well, default.
formatter #
The formatter is a single derivation that is run when nix fmt
is executed. Most often, one of pkgs.alejandra
, pkgs.nixfmt
or pkgs.nixpkgs-fmt
is used. The upcoming standard will be pkgs.nixfmt-rfc-style
and I recommend checking out Overview of Nix formatters ecosystem for more information.
# Not very well formatted code.
# It's a single line with inconsistent spacing around stuff.
{ outputs = {nixpkgs, self }: {formatter.x86_64-linux=nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;};}
❯ nix fmt
# Much better
{
outputs =
{ nixpkgs, self }:
{
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
};
}
Note, however, that this attribute is not limited to formatting only nix code. It is possible to use a tool like treefmt to format code in arbitrary language when nix fmt
is called.
hydraJobs #
This output is used by hydra – a tool for continuous integration used for NixOS. Hydra can use this attribute to create and evaluate jobs. The wiki article provides a very good example.
bundlers #
Bundlers are functions that allow post-processing derivations, for example, to pack a derivation so that it can be run on a non-NixOS system.
They are used by nix bundle
(man page) and have a dedicated repo
with example.
System-independent outputs #
overlays #
The main purpose of a Nix overlay is to push new or overwrite attributes of pkgs
. As an example, the flake below adds a custom package called foo
so that the consumer (in this case, overlayTest
check inside the same flake) can just add the overlay when initializing pkgs
and call pkgs.foo
. Without the overlay pkgs.foo
would (at the time of writing) produce “attribute ‘foo’ missing”.
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.foo = pkgs.writeShellScriptBin "foo" "echo -n 'I am a custom package'";
overlays.default = final: prev: { inherit (self.packages.${prev.system}) foo; };
checks.x86_64-linux.overlayTest = pkgs.testers.runNixOSTest {
name = "overlayTest";
node.pkgsReadOnly = false; # Option that is special for tests where machines need to change their nixpkgs
nodes.machine1 = { pkgs, ... }: { # A module in the resulting system that is defined in line
nixpkgs.overlays = [ self.overlays.default ];
environment.systemPackages = [ pkgs.foo ];
};
testScript =
# See the devmanual for available python methods:
# https://nixos.org/manual/nixos/stable/#ssec-machine-objects
''
assert machine.execute("foo")[1] == "I am a custom package"
'';
};
};
}
nixosConfigurations #
nixosConfigurations
exist to be consumed by nixos-rebuild --flake <flakeref>#<hostname> <other nixos-rebuild options>
. With this, we can manage the entire (possibly, even remote) NixOS machine. This output is the first candidate to be set if migrating from the standard NixOS install with the system configuration under /etc/nixos
.2
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.foo = pkgs.writeShellScriptBin "foo" "echo -n 'I am a custom package'";
nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
(
{ pkgs, ... }:
{
environment.systemPackages = [
pkgs.hello
self.packages.${pkgs.system}.foo
];
}
)
./configuration.nix
./other-modules.nix
{
# Stub options
fileSystems."/".device = "/dev/sda";
boot.loader.grub.devices = [ "/" ];
system.stateVersion = "24.05";
}
];
};
};
}
nixosConfigurations
can also be run as interactive virtual machines by either:
❯ nixos-rebuild build-vm --flake .#my-machine
# or
❯ nix build .#nixosConfigurations.my-machine.config.system.build.vm
templates #
Temlpates are meant to be used by nix flake init -t <flakeref>#<templateref>
. An example of a flake that clones itself when nix flake init
is called:
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.foo = pkgs.writeShellScriptBin "foo" "echo -n 'I am a custom package'";
templates.default = {
description = "My self-cloning template";
path = builtins.toPath self;
welcomeText = "This template is an example. *Markdown works too*, depending on your terminal emulator.";
};
};
}
❯ cd $(mktemp -d)
❯ ls
# empty
❯ nix flake init -t /tmp/tmp.fe06VZ7KtP
wrote: /tmp/tmp.Yr6JQB8UOE/flake.nix
wrote: /tmp/tmp.Yr6JQB8UOE/flake.lock
This template is an example. Markdown works too, depending on your terminal emulator.
❯ nix flake show
path:/tmp/tmp.<boring>
├───packages
│ └───x86_64-linux
│ └───foo: package 'foo'
└───templates
└───default: template: My self-cloning template
nixosModules #
Flakes can provide NixOS modules that can be units of a system’s configuration or provide configuration for the custom packages.
Here is an example of a flake with:
- A simple custom package runnable through
nix run
. The package optionally reads/etc/parametrized-foo
file and prints its contents - A NixOS module which enables (read: adds to
$PATH
) the custom package and populates/etc/parametrized-foo
file - Two checks that validate the package and the module independently by running the command and checking its output
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.default = pkgs.writeShellScriptBin "foo" "echo -n 'I am a custom package'";
nixosModules.default =
{ pkgs, ... }:
{
environment.systemPackages = [ self.packages.${pkgs.system}.default ];
};
checks.x86_64-linux = {
# If for whatever reason the package definition does not contain integration tests
packageTest = pkgs.stdenv.mkDerivation {
name = "packageTest";
dontUnpack = true;
dontBuild = true;
doCheck = true;
nativeBuildInputs = [ self.packages.x86_64-linux.default ];
checkPhase = ''
if [ "$(foo)" = "I am a custom package" ]; then
echo "OK"
else
echo "NOK"
exit 1
fi
'';
installPhase = "mkdir $out"; # "$out" needs to be produced by a derivation, otherwise check will not pass the check
};
# Check the module
moduleTest = pkgs.testers.runNixOSTest {
name = "moduleTest";
nodes.machine1 = {
imports = [ self.nixosModules.default ];
};
testScript =
# See the devmanual for available python methods:
# https://nixos.org/manual/nixos/stable/#ssec-machine-objects
''
assert machine.execute("foo")[1] == "I am a custom package"
'';
};
};
};
}
home-manager modules #
Despite not being a part of the standard flake schema and having various names like homeManagerModules
or hmModules
, home manager modules are quite commonly encountered in per-project flakes.
As standard NixOS modules, they are functions. Unlike NixOS modules they need the rest of the home-manager modules to work and need to be imported in the context of a specific user.
Here is a flake that’s very similar to the one in nixosModules. It has:
- A simple custom package runnable through
nix run
. The package optionally reads~/.config/parametrized-foo
file and prints its contents - A home manager module which enables (read: adds to
$PATH
) the custom package and populates~/.config/parametrized-foo
file - Two checks that validate the package and the module independently by running the command and checking its output
{
inputs.home-manager.url = "github:nix-community/home-manager";
outputs =
{ nixpkgs, self, home-manager }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux.parametrized-foo = pkgs.writeShellScriptBin "parametrized-foo" ''
echo "This is a sample program that tries to read a file from ~/.config."
cat $HOME/.config/parametrized-foo || echo "File not found in ~/.config"
'';
homeManagerModules.default =
{
pkgs,
lib,
config,
...
}:
let
cfg = config.programs.parametrized-foo;
inherit (lib) mkEnableOption mkOption types;
in
{
options.programs.parametrized-foo = {
enable = mkEnableOption "parametrized-foo";
etc-content = mkOption {
# Use RFC42 for proper packages: https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
type = types.str;
description = "Content of the config";
};
};
config = lib.mkIf cfg.enable {
xdg.configFile."parametrized-foo".text = cfg.etc-content;
home.packages = [ self.packages.${pkgs.system}.parametrized-foo ];
};
};
checks.x86_64-linux = {
packageTest = pkgs.stdenv.mkDerivation {
name = "packageTest";
dontUnpack = true;
dontBuild = true;
doCheck = true;
nativeBuildInputs = [ self.packages.x86_64-linux.parametrized-foo ];
checkPhase = ''
EXPECTED_OUTPUT="This is a sample program that tries to read a file from ~/.config.
File not found in ~/.config"
if [ "$(parametrized-foo 2>/dev/null)" = "$EXPECTED_OUTPUT" ]; then
echo "OK"
else
echo "NOK"
exit 1
fi
'';
installPhase = "mkdir $out"; # "$out" needs to be produced by a derivation, otherwise check will not pass the check
};
moduleTest = pkgs.testers.runNixOSTest {
name = "moduleTest";
nodes.machine1 = {
imports = [ home-manager.nixosModules.home-manager ];
users.users.alice = {
isNormalUser = true;
};
home-manager.users.alice =
_: # { config, ... }: # config is home-manager's config, not the OS one
{
imports = [ self.homeManagerModules.default ];
home.stateVersion = "24.05";
programs.parametrized-foo = {
enable = true;
etc-content = "bar";
};
};
};
testScript = ''
machine.wait_for_unit("default.target")
# This test executes a command "parametrized-foo" in the VM as "alice" user and checks that its output is "This is a sample program that tries to read a file from ~/.config.\nbar"
# where "bar" is the part that's set through the option above
assert machine.execute("su - alice bash -c 'parametrized-foo'")[1] == "This is a sample program that tries to read a file from ~/.config.\nbar"
'';
};
};
};
}
lib and other non-standard outputs #
Flake outputs are a proper nix attrset – so it is common to see some functions which build on top of the nix syntax to be available as .lib
output. The standard flake schema does not recognize these outputs, so nix flake check
emits a warning.
I’ve personally not encountered checks for lib
output, but have done something like this:
{
outputs =
{ nixpkgs, self }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
/**
Always returns false for any input.
*/
lib.myAwesomeFunc = _: true;
checks.x86_64-linux = {
testAwesomeFunc = pkgs.stdenv.mkDerivation {
name = "testAwesomeFunc";
dontUnpack = true;
dontBuild = true;
# If the test fails, $out is not created => derivation produces an error
installPhase =
if (self.lib.myAwesomeFunc 1) then
"mkdir $out" # "$out" needs to be produced by a derivation, otherwise check will not pass the check
else
"";
};
};
};
}
nixConfig #
This top-level flake attribute allows specifying nix.conf
values for the flake. In my experience it is most often used to specify extra-substituters
so that flake’s consumers can get pre-compiled packages from caches other than cache.nixos.org
.
This attribute is entirely optional.
Frequently encountered code patterns #
There are certain code patterns that are commonly used in flakes that I feel like don’t get explained enough.
self
as an argument for outputs
#
self
is a reference to the flake itself. This is how the flake components (usually outputs) can reference other flake components.
Consider this flake:
{
outputs =
{ self }:
{
my-output = builtins.trace (self) true;
my-other-output = _: false;
};
}
❯ nix eval .#my-output
trace: { _type = "flake"; inputs = { }; lastModified = 1711942013; lastModifiedDate = "20240401032653"; my-other-output =
«thunk»; my-output = «potential infinite recursion»; narHash = "sha256-5e+ExKiSrYBz5BUZRZkBIDUs2W9wsbn9u7QbMWtmUqU="; ou
tPath = "/nix/store/79q1s4jwvn2j9ssc8n3j33mvdjn14rgq-source"; outputs = { my-other-output = «thunk»; my-output = «potenti
al infinite recursion»; }; sourceInfo = { lastModified = 1711942013; lastModifiedDate = "20240401032653"; narHash = "sha2
56-5e+ExKiSrYBz5BUZRZkBIDUs2W9wsbn9u7QbMWtmUqU="; outPath = "/nix/store/79q1s4jwvn2j9ssc8n3j33mvdjn14rgq-source"; }; }
true
If self
is not really needed in the flake, it can be hidden behind ...
in arguments for outputs
. This makes linters such as deadnix happy.
{
outputs = { ... }: { my-output = true; };
}
Adding <flakeref>
as an input without specifying it
#
Inputs can be implicitly added to the arguments of outputs. As long as they can be resolved from the flake registry, nix will add them to the flake lockfile.
Creating a flake like this:
{ outputs = { nixpkgs, ... }: { }; }
and running nix flake lock
will add nixpkgs
to the lockfile.
inputs@{ ... }
syntax
#
When the list of inputs grows, it might become impractical to specify every single input in arguments for outputs. Nix has the "@ pattern", described in the docs.
Often the name inputs
is used, which is an (unwritten?) convention. But any other name works:
{
inputs.nixpkgs.url = "nixpkgs/nixos-23.11";
outputs =
args@{ ... }:
# args@{ nixpkgs, ... }: # This would also work
{
packages.x86_64-linux.default = args.nixpkgs.legacyPackages.x86_64-linux.hello;
};
}
flake-utils, flake.parts, etc. #
Certain parts of the flake require its developer to write a bit of boilerplate. Let’s say we want to provide a package for multiple architectures.
The straightforward way is:
{
outputs =
{ nixpkgs, ... }:
{
packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.hello;
packages.aarch64-linux.default = nixpkgs.legacyPackages.aarch64-linux.hello;
};
}
But expanding it beyond just two systems and packages becomes tedious.
We could take advantage of nix’s dynamic nature by making packages
a result of a function (adapted from bash flake template):
{
outputs =
{ nixpkgs, ... }:
let
supportedSystems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
packages = forAllSystems (system: {
default = (nixpkgsFor.${system}) hello;
});
};
}
But copying around this boilerplate would be kinda boring. Let’s reuse it from an existing library, flake-utils
:
{
outputs =
{ flake-utils, nixpkgs, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.default = pkgs.hello;
}
);
}
This looks much more neat. One problem with such approaches is that libraries like flake-utils
define their own domain specific language built on top of nix. This language would need some extra time to get adjusted to.
Specific to the flake-utils
use is that flake-utils.lib.eachDefaultSystem
function gets copied around and folks are defining system-independent outputs inside this function. While this is valid code:
{
outputs =
{ flake-utils, nixpkgs, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.default = pkgs.hello;
nixosModules.default = _: { };
}
);
}
And it even passes nix flake check
, the consumer would need to import <flakeref>.nixosModules.<system>.default
rather than <flakeref>.nixosModules.default
. One way around this is to merge the system-independent attributes to the result of flake-utils.lib.eachDefaultSystem
:
{
outputs =
{ flake-utils, nixpkgs, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.default = pkgs.hello;
}
)
// {
nixosModules.default = _: { };
};
}
An alternative to flake-utils
, flake.parts
, is more explicit about the type of outputs:
{
outputs =
inputs@{ flake-parts, nixpkgs, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ ];
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
perSystem =
{ pkgs, ... }:
{
packages.default = pkgs.hello;
};
flake = {
nixosModules.default = _: { };
};
};
}
It also does a few other convenient things behind the scenes (like magically creating pkgs
, allowing composing flakes like modules, etc.). Its DSL does take a bit of getting used to though and requires knowing the basics first.
Deprecated outputs #
Certain outputs (defaultPackage
, nixosModule
and the like) have been
deprecated in nix PR #5532 but still may be encountered in older flakes.
nix flake check
emits a warning like “flake output attribute <name> is deprecated
”.
For example, this is a way to see all warnings:
{
outputs =
{ self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in
{
# System-specific outputs
defaultPackage.${system} = pkgs.hello; # packages.${system}.default
devShell.${system} = pkgs.mkShell { natveBuildInputs = [ pkgs.hello ]; }; # devShells.${system}.default
defaultApp.${system} = { # apps.${system}.default
type = "app";
program = pkgs.lib.getExe pkgs.hello;
};
defaultBundler.${system} = drv: drv; # bundlers.${system}.default
# System-independent outputs
overlay = final: prev: { inherit (pkgs) hello; }; # overlays.default
nixosModule = _: { }; # nixosModules.default
defaultTemplate = { # templates.default
description = "Default template";
path = builtins.toPath self;
};
};
}
And:
❯ nix flake check
warning: flake output attribute 'defaultPackage' is deprecated; use 'packages.<system>.default' instead
warning: flake output attribute 'devShell' is deprecated; use 'devShells.<system>.default' instead
warning: flake output attribute 'defaultApp' is deprecated; use 'apps.<system>.default' instead
warning: flake output attribute 'defaultBundler' is deprecated; use 'bundlers.<system>.default' instead
warning: flake output attribute 'overlay' is deprecated; use 'overlays.default' instead
warning: flake output attribute 'nixosModule' is deprecated; use 'nixosModules.default' instead
warning: flake output attribute 'defaultTemplate' is deprecated; use 'templates.default' instead
Further reading #
- Flake format
- NixOS & Flakes Book for a more in-depth dive into flakes and adjacent development techniques
- Flakes entry on NixOS wiki
- Practical Nix flakes
-
but dont’t strictly need to; see non-standard outputs. ↩︎
-
if a flake is managed in a git repository, then files outside of that repository cannot be included – they need to be copied into the repo ↩︎