6.8 KiB
+++ title="Generate Nix Flake "follows"" template="custom/blog/page.html" +++ Like many Nix users I am using the "experimental" flakes feature1. Flakes are a way of defining inputs and outputs:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
# other inputs
};
outputs = inputs @ {nixpkgs, ...}: {
# imagine some output here
};
}
The outputs are used (as the name implies) for some kind of output, such as: packages, NixOS configuration, development shells, ...
But as this blog post exclusively revolves around the inputs part you can already forget about outputs again.
The inputs are a reference to a source, like a git repo, which are used by the flake. Most often you want to have one input for nixpkgs, where most of the package definitions for Nix are found. Additionally you will likely use some Nix libraries which are also inputs to your flake. You can also track source code of projects which do not use Nix at all, which allows for the equivalent of "-git" packages in the AUR. To make flakes reproducible the specific version of each input is recorded in the "flake.lock" file.
Inputs all the way down
The example flake above is so simple that you will hardly see something alike in the wild. As a real world example we will look at the flake I use to define the configurations of all my machines. In it I use many libraries and NixOS modules2 which leads to an inputs section like this:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
clan-core.url = "git+https://git.clan.lol/clan/clan-core";
clan-core.inputs.nixpkgs.follows = "nixpkgs"; # Needed if your configuration uses nixpkgs unstable.
flake-utils.url = "github:numtide/flake-utils";
git-hooks.url = "github:cachix/git-hooks.nix";
home-manager.url = "github:nix-community/home-manager";
impermanence.url = "github:nix-community/impermanence";
jovian.url = "github:Jovian-Experiments/Jovian-NixOS";
lix-module.url = "https://git.lix.systems/lix-project/nixos-module/archive/2.93.0.tar.gz";
nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
nur.url = "github:nix-community/NUR";
niri.url = "github:sodiboo/niri-flake";
};
outputs = _: {};
}
In reality I use even more inputs, but you get the point.
And to complicate things further all of these inputs seen above are also flakes. This means they can define inputs, which could in turn be flakes, which could define inputs, and so on. As eluded to earlier we need to keep track of each inputs version to ensure reproducibility.
But with so many inputs which also define inputs we inevitably end up with duplicates. In some cases this can be exactly what we want to ensure we build the same thing as the person who defined the input we use. However often we do not want 10 different versions of nixpkgs and in turn 10 versions of bash or other packages. The evaluation time also increases with the number of distinct inputs.
I will follow you
To solve this problem we can use the follows feature. I already used this in the second example to make the nixpkgs of clan-core follow our nixpkgs defined above. This causes nix to override the nixpkgs version of clan-core to the same one as the nixpkgs input.
For a handful of inputs this can be easily written by hand, but at some point becomes tiring. The flake defined in the previous section uses a staggering 49 inputs3. I definitely do not want to figure out and write all the follow statements for this by hand!
Just One Line
To get an overview for inputs which are redundant I wrote a jq one-liner4:
nix flake metadata --json | jq -r \
'.locks.nodes
| map(select(has("inputs"))
| .inputs[])
| flatten
| map(select(test(".*_[0-9]+")))[]'
Resulting in the following candidates for duplicate inputs:
systems_2
systems_3
gitignore_2
nixpkgs_2
nixpkgs_3
flake-utils_2
nixpkgs_4
nixpkgs_5
flake-compat_2
git-hooks_2
nixpkgs_6
nixpkgs-24_11
flake-parts_3
nixpkgs_8
treefmt-nix_2
flake-parts_2
nixpkgs_7
In this list nixpkgs-24_11 is a false positive, but the rest are duplicates.
To avoid these duplicate inputs I wrote another one-liner.
nix flake metadata --json | jq -r \
'(.locks.nodes.root.inputs | keys) as $root_input_keys
| .locks.nodes | to_entries[]
| select(.key | IN($root_input_keys[]))
| select((.value | has("flake") | not) or (.value.flake == true))
| select(.value | has("inputs"))
| .key as $parent_key
| .value.inputs | keys[]
| select(IN($root_input_keys[]))
| $parent_key + ".inputs." + . + ".follows = \"" + . + "\";"'
It generates a follow statement for every non top-level input with the same name as a top-level input. Looking at our example we already have a top-level input called nixpkgs. This means the script will create a follow statement for every other input called nixpkgs to follow this top-level nixpkgs.
Running this script produces a list of follow statements:
clan-core.inputs.flake-parts.follows = "flake-parts";
clan-core.inputs.nixpkgs.follows = "nixpkgs";
git-hooks.inputs.nixpkgs.follows = "nixpkgs";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
jovian.inputs.nixpkgs.follows = "nixpkgs";
lix-module.inputs.flake-utils.follows = "flake-utils";
lix-module.inputs.nixpkgs.follows = "nixpkgs";
niri.inputs.nixpkgs.follows = "nixpkgs";
nixos-mailserver.inputs.git-hooks.follows = "git-hooks";
nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
nur.inputs.flake-parts.follows = "flake-parts";
nur.inputs.nixpkgs.follows = "nixpkgs";
By adding these to our flake we reduce the number of inputs to 36.
We can reduce this further by adding new top-level inputs which are used often by the libraries we use. To do so we can again identify duplicates, add them as top-level imports and run the script again.
Conclusion
We were able to reduce our number of inputs from 49 inputs to 36 (which could be reduced even further). This should speed up evaluation and avoid some duplicate packages for our flake.
There exist a similar tool nix-auto-follow which rewrites the "flake.lock" file directly to remove duplicates. However by rewriting the lock file it is harder to edit/change what inputs follows which. There is an open issue to make it more configurable. So maybe this problem could be solved in the future.
For now tough I will just use my trusty one-liner!