zerforschen.plus/content/posts/why-i-do-not-use-flake-utils.md
2025-04-09 23:58:25 +02:00

4.1 KiB
Raw Blame History

+++ date = '2025-04-06T14:49:03+02:00' draft = false title = 'Why I do not use flake-utils' tags = ['nix'] +++

I have been using Nix for a while now. Around a year ago, I switched everything from the servicepoint library to my machine configuration over to flakes.

For me the biggest advantage flakes bring is not additional functionality. Instead, they are an easier and semi-standardized way to do what you could before.

When learning flakes, you often see flake-utils being used. With it, you can shorten your flakes by not having to specify everything per system.

Without anything

{
  description = "Flake utils demo - without flakes";

  outputs = { self, nixpkgs, flake-utils }:
  {
    packages."x86_64-linux" = rec {
          hello = pkgs.hello;
          default = hello;
    }
    packages."aarch64-linux" = rec {
          hello = pkgs.hello;
          default = hello;
    }

    # more systems ...
  }
}

With flake-utils

{
  description = "Flake utils demo";

  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system}; in
      {
        packages = rec {
          hello = pkgs.hello;
          default = hello;
        };
      }
    );
}

With function in flake

To make a long story short, here is what I usually do instead:

{
  description = "forAllSystems demo";

  outputs = { self, nixpkgs }:
    let
      supported-systems = [ # [1]
        "x86_64-linux"
        "aarch64-linux"
      ];
      forAllSystems =
        f:
        nixpkgs.lib.genAttrs supported-systems /* [2] */ (
          system:
          f rec {
            pkgs = nixpkgs.legacyPackages.${system}; # [3]
            inherit system;
          }
        );
    in
    {
      packages = forAllSystems ({ pkgs, ... }: rec {
        hello = pkgs.hello;
        default = default;
      });
    }
}

Thats definitely more code! - Yes, but it also includes more information in the flake, while getting rid of an external dependency. While more code can be intimidating for beginners, it actually helps remove a barrier to understanding how the flake works in this case. For me, it wasnt a problem to ignore boilerplate like this at first, slowly learning more language features until I finally understood everything.

At [1], the supported systems are specified. I personally use x86_64-linux and aarch64-linux, but I also usually support x86_64-darwin and aarch64-darwin in public projects. If you want to support any system, you can use nixpkgs.lib.system.flake-exposed at [2] instead of defining your own list.

Because the definition is right inside the flake, you can tweak what gets passed to the function. For example, the flake for RedoxOS-development I contributed1 this to passes the custom rust-toolchain. An example for how to do it is already right there: at [3], pkgs is provided.

Another possible tweak: You may want to define separate supported systems for each output. This is useful, for example, if the target environment you're developing for cannot support a development shell.

For me, the trade-offs are worth it, as they provide greater transparency and control over the flake configuration.

That being said, I fully acknowledge that flake-utils can still be a great choice for many people. It simplifies things and reduces the need to write boilerplate code, which can be a plus depending on your needs and workflow. Ultimately, it's also matter of personal preference.


  1. If you check the history, you will see I am not mentioned. I am still a bit salty about that, as it was my first contribution to a bigger OSS project. ↩︎