+++ 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 ```nix { 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 ```nix { 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: ```nix { 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; }); } } ``` That’s 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 wasn’t 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 contributed[^1] 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 big plus depending on your needs and workflow. Ultimately, it's a matter of personal preference and the specific requirements of your project. [^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. [Nix]: https://nixos.org/ [servicepoint]: https://git.berlin.ccc.de/servicepoint/servicepoint [machine configuration]: https://git.berlin.ccc.de/vinzenz/nixos-configuration [flake-utils]: https://github.com/numtide/flake-utils [`nixpkgs.lib.system.flake-exposed`]: https://github.com/NixOS/nixpkgs/blob/374e6bcc403e02a35e07b650463c01a52b13a7c8/lib/systems/default.nix#L58 [flake for RedoxOS-development]: https://gitlab.redox-os.org/redox-os/redox/-/blob/cb34b9bd862f46729c0082c37a41782a3b1319c3/flake.nix#L38