Flakes
Nix allows you to define exactly how a particular program is build, tested and installed. The end result is what's called a derivation, which explicitly defines inputs (what does this program depend on), and outputs (the binary for example).
The inputs can be at build or runtime. For example in order to
statically compile a binary that uses libyaml
you can specify
that it needs libyaml
at build time but not at runtime.
Flakes are a Nix feature that allows you to do that while pinning the input versions.
In this short post we are gonna define a flake for my static blog.
Our Flake
A flake has three main top level attributes
{
description = "Flake description";
inputs = { ... };
outputs = { ... };
}
The description
is fairly self-explanatory, inputs
is a set of
inputs that this flake will take, everything we use needs to be in
this set, there are no globals, no implicit packages, etc. outputs
defines what this flake provides.
Inputs
Let's see how inputs are defined, when working in non-flake Nix we
usually have <nixpkgs>
imported by default, so we have access to the
whole package repository of nix. In flakes we have to set nixpkgs
explicitly as an input if we need to.
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
}
I also import a couple of helpers that help us write flakes.
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/default";
}
When nix sees this, it goes to those repositories and looks for
a flake.nix
file. For example here
is the one for flake-parts
. You should mostly understand what
is going on there at least at a high level. Inputs and outputs.
Everything expose in those flakes will be available in outputs
under inputs.<input-name>.*
.
Outputs
We have a single output for our blog, the rendered blog. In my case my blog is rendered using Zola.
Let's start by writing the nix derivation that builds the blog, this is flake-agnostic. Then later we are gonna integrate it into the flake.
Derivation
A nix derivation should be pretty familiar to most software engineers, you explicityly state dependencies (in different attributes, depending on when the dependency is needed), and the build steps.
Let's see the derivation with some clarifying comments:
pkgs.stdenv.mkDerivation {
name = "dziban-blog";
# depsBuildBuild are used only at build time, the resulting
# derivation has no dependency on it.
depsBuildBuild = [ pkgs.zola ];
# Specify where to get the source, this will be copied to the
# $src directory inside the build sandbox.
src = ./.;
# Shell script that builds whatever is needed. It has
# depsBuildBuild dependencies available
buildPhase = ''
zola build
'';
# Shell script that installs whatever is needed.
# We have an envvar $out that is the directory where
# nix will expect all the results of building the derivation
installPhase = ''
mkdir -p $out
mv public/* $out
'';
}
It's fairly straightforward, we define that zola
is a dependency at
build time, specify that the source is everything in the current
directory, then we build and install.
The whole output expression
Now let's add this to the flake outputs, we use a flake-parts library
called mkFlake
that helps us in several ways that I won't go now,
you can check their website for more info.
But basically you give it the systems this flake is available in (for
example x86_64-linux
) and then define what outputs you have per system.
{
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = import inputs.systems;
perSystem = { config, self', pkgs, ... }: {
packages = {
dziban-blog = pkgs.stdenv.mkDerivation { ... };
default = self'.packages.dziban-blog;
};
};
};
}
Instead of hardcoding the systems we added the systems flake to do that,
we pass it to the systems
attribute of mkFlake
. Then perSystem
runs
for each system we defined and in this case defines only packages
outputs.
packages.dziban-blog
is the derivation we wrote above. default
is
the package that nix looks for when one is not specified when building
or installing, we just point it to the dziban-blog
package.
Building
To build a flake you just run nix build
. Nix will gather the inputs, create
a flake.lock
with the pinned versions of dependencies and execute the nix
expression. In this case that means building the derivation we defined.
The $out
directory is then symbolically linked in the current directory
as ./result
and it contains the whole content of public/
after building.
[marcecoll@nixos:~/proj/dziban]$ ls -l ./result
lrwxrwxrwx 1 marcecoll users 55 abr 23 13:13 ./result -> /nix/store/512fd9y2l9xs1m5vgd2i7wsdchr8pb4q-dziban-blog
[marcecoll@nixos:~/proj/dziban]$ ls ./result
404.html blog essays images posts search_index.en.js style.css
atom.xml elasticlunr.min.js exploratory-programming index.html robots.txt sitemap.xml
We can also inspect the resulting derivation, the result is very long but I'm gonna highlight some parts of it
[marcecoll@nixos:~/proj/dziban]$ nix derivation show ./result
{
"name": "dziban-blog",
"outputs": {
"out": {
"path": "/nix/store/512fd9y2l9xs1m5vgd2i7wsdchr8pb4q-dziban-blog"
}
},
"system": "x86_64-linux"
The name and current system plus the output path. You can see it's the same that
./result
is linked to.
"inputDrvs": {
"/nix/store/96j7mvs5427kbfbg02zbf3586gnsr7b6-stdenv-linux.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/jnm2249gyarbs1nir7mzam257fgamrpf-bash-5.2p26.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
},
"/nix/store/yvadchlyfi4m9xwz8bvw7mcnyys30z0j-zola-0.18.0.drv": {
"dynamicOutputs": {},
"outputs": [
"out"
]
}
},
These are the inputs to the derivation, bash is needed to run the steps, stdenv contains the mkDerivation code and zola is used to build the blog.
The Whole Flake
{
description = "blog.dziban.net";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
systems.url = "github:nix-systems/default";
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = import inputs.systems;
perSystem = { config, self', pkgs, ... }: {
packages = {
dziban-blog = pkgs.stdenv.mkDerivation {
name = "dziban-blog";
depsBuildBuild = [ pkgs.zola ];
src = ./.;
buildPhase = ''
zola build
'';
installPhase = ''
mkdir -p $out
mv public/* $out/
'';
};
default = self'.packages.dziban-blog;
};
};
};
}
In a future post I'm gonna talk about how to take this flake and easily deploy it on a NixOS server.